Browse Source

Merge branch 'v2_develop' of tig:gui-cs/Terminal.Gui into v2_develop

Tigger Kindel 2 years ago
parent
commit
e6c8d21981
100 changed files with 4450 additions and 3223 deletions
  1. 2 2
      .github/workflows/dotnet-core.yml
  2. 7 7
      ReactiveExample/LoginView.cs
  3. 7 7
      ReactiveExample/LoginViewModel.cs
  4. 1 1
      ReactiveExample/README.md
  5. 25 96
      Terminal.Gui/Application.cs
  6. 12 8
      Terminal.Gui/Clipboard/Clipboard.cs
  7. 29 32
      Terminal.Gui/Configuration/AppScope.cs
  8. 1 3
      Terminal.Gui/Configuration/AttributeJsonConverter.cs
  9. 1 3
      Terminal.Gui/Configuration/ColorJsonConverter.cs
  10. 1 1
      Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs
  11. 97 0
      Terminal.Gui/Configuration/ConfigProperty.cs
  12. 460 564
      Terminal.Gui/Configuration/ConfigurationManager.cs
  13. 1 1
      Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs
  14. 4 1
      Terminal.Gui/Configuration/DictionaryJsonConverter.cs
  15. 1 6
      Terminal.Gui/Configuration/KeyJsonConverter.cs
  16. 119 0
      Terminal.Gui/Configuration/RuneJsonConverter.cs
  17. 47 184
      Terminal.Gui/Configuration/Scope.cs
  18. 140 0
      Terminal.Gui/Configuration/ScopeJsonConverter.cs
  19. 28 0
      Terminal.Gui/Configuration/SerializableConfigurationProperty.cs
  20. 97 99
      Terminal.Gui/Configuration/SettingsScope.cs
  21. 207 0
      Terminal.Gui/Configuration/ThemeManager.cs
  22. 50 269
      Terminal.Gui/Configuration/ThemeScope.cs
  23. 43 360
      Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs
  24. 21 10
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  25. 2 1
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs
  26. 0 2
      Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs
  27. 12 12
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  28. 24 15
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  29. 36 25
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  30. 20 0
      Terminal.Gui/Drawing/Cell.cs
  31. 670 0
      Terminal.Gui/Drawing/Glyphs.cs
  32. 140 90
      Terminal.Gui/Drawing/LineCanvas.cs
  33. 2 3
      Terminal.Gui/Drawing/Ruler.cs
  34. 21 16
      Terminal.Gui/Drawing/Thickness.cs
  35. 13 2
      Terminal.Gui/FileServices/AllowedType.cs
  36. 1 1
      Terminal.Gui/FileServices/DefaultFileOperations.cs
  37. 35 0
      Terminal.Gui/FileServices/FileDialogIconGetterArgs.cs
  38. 18 0
      Terminal.Gui/FileServices/FileDialogIconGetterContext.cs
  39. 7 3
      Terminal.Gui/FileServices/FileDialogRootTreeNode.cs
  40. 1 1
      Terminal.Gui/FileServices/FileDialogState.cs
  41. 37 13
      Terminal.Gui/FileServices/FileDialogStyle.cs
  42. 34 4
      Terminal.Gui/FileServices/FileDialogTreeBuilder.cs
  43. 2 28
      Terminal.Gui/FileServices/FileSystemInfoStats.cs
  44. 1 1
      Terminal.Gui/Input/KeystrokeNavigatorEventArgs.cs
  45. 13 14
      Terminal.Gui/Input/ShortcutHelper.cs
  46. 0 13
      Terminal.Gui/MainLoop.cs
  47. 9 5
      Terminal.Gui/Resources/config.json
  48. 79 0
      Terminal.Gui/RunState.cs
  49. 0 32
      Terminal.Gui/StringExtensions.cs
  50. 2 2
      Terminal.Gui/Terminal.Gui.csproj
  51. 19 23
      Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs
  52. 1 1
      Terminal.Gui/Text/Autocomplete/AutocompleteContext.cs
  53. 1 1
      Terminal.Gui/Text/Autocomplete/IAutocomplete.cs
  54. 1 1
      Terminal.Gui/Text/Autocomplete/ISuggestionGenerator.cs
  55. 4 3
      Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs
  56. 3 3
      Terminal.Gui/Text/Autocomplete/SingleWordSuggestionGenerator.cs
  57. 16 201
      Terminal.Gui/Text/CollectionNavigator.cs
  58. 217 0
      Terminal.Gui/Text/CollectionNavigatorBase.cs
  59. 311 0
      Terminal.Gui/Text/RuneExtensions.cs
  60. 154 0
      Terminal.Gui/Text/StringExtensions.cs
  61. 35 0
      Terminal.Gui/Text/TableCollectionNavigator.cs
  62. 143 145
      Terminal.Gui/Text/TextFormatter.cs
  63. 23 0
      Terminal.Gui/Timeout.cs
  64. 6 9
      Terminal.Gui/Types/Rect.cs
  65. 81 52
      Terminal.Gui/View/Frame.cs
  66. 1 0
      Terminal.Gui/View/Layout/PosDim.cs
  67. 4 4
      Terminal.Gui/View/TitleEventArgs.cs
  68. 12 12
      Terminal.Gui/View/View.cs
  69. 77 64
      Terminal.Gui/View/ViewDrawing.cs
  70. 5 0
      Terminal.Gui/View/ViewEventArgs.cs
  71. 6 6
      Terminal.Gui/View/ViewKeyboard.cs
  72. 22 28
      Terminal.Gui/View/ViewLayout.cs
  73. 1 1
      Terminal.Gui/View/ViewMouse.cs
  74. 1 1
      Terminal.Gui/View/ViewSubViews.cs
  75. 9 9
      Terminal.Gui/View/ViewText.cs
  76. 10 11
      Terminal.Gui/Views/AutocompleteFilepathContext.cs
  77. 23 30
      Terminal.Gui/Views/Button.cs
  78. 16 16
      Terminal.Gui/Views/CheckBox.cs
  79. 117 76
      Terminal.Gui/Views/ColorPicker.cs
  80. 20 18
      Terminal.Gui/Views/ComboBox.cs
  81. 5 14
      Terminal.Gui/Views/ContextMenu.cs
  82. 44 40
      Terminal.Gui/Views/DateField.cs
  83. 4 2
      Terminal.Gui/Views/Dialog.cs
  84. 45 10
      Terminal.Gui/Views/FileDialog.cd
  85. 207 371
      Terminal.Gui/Views/FileDialog.cs
  86. 75 0
      Terminal.Gui/Views/FileDialogTableSource.cs
  87. 4 4
      Terminal.Gui/Views/FrameView.cs
  88. 1 25
      Terminal.Gui/Views/GraphView/Annotations.cs
  89. 9 8
      Terminal.Gui/Views/GraphView/Axis.cs
  90. 38 0
      Terminal.Gui/Views/GraphView/BarSeriesBar.cs
  91. 1 0
      Terminal.Gui/Views/GraphView/GraphCellToRender.cs
  92. 16 17
      Terminal.Gui/Views/GraphView/GraphView.cs
  93. 25 0
      Terminal.Gui/Views/GraphView/LineF.cs
  94. 8 44
      Terminal.Gui/Views/GraphView/Series.cs
  95. 8 7
      Terminal.Gui/Views/HexView.cs
  96. 1 1
      Terminal.Gui/Views/HistoryTextItem.cs
  97. 14 0
      Terminal.Gui/Views/ITreeViewFilter.cs
  98. 5 5
      Terminal.Gui/Views/Label.cs
  99. 5 6
      Terminal.Gui/Views/Line.cs
  100. 16 17
      Terminal.Gui/Views/LineView.cs

+ 2 - 2
.github/workflows/dotnet-core.yml

@@ -7,7 +7,7 @@ on:
     branches: [ main, develop, v2_develop ]
 
 jobs:
-  build:
+  build_and_test:
 
     runs-on: ubuntu-latest
 
@@ -17,7 +17,7 @@ jobs:
     - name: Setup .NET Core
       uses: actions/[email protected]
       with:
-        dotnet-version: 6.0.100
+        dotnet-version: 7.0.x
 
     - name: Install dependencies
       run: |

+ 7 - 7
ReactiveExample/LoginView.cs

@@ -1,6 +1,6 @@
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
-using NStack;
+using System.Text;
 using ReactiveUI;
 using Terminal.Gui;
 using ReactiveMarbles.ObservableEvents;
@@ -64,7 +64,7 @@ namespace ReactiveExample {
 			};
 			ViewModel
 				.WhenAnyValue (x => x.UsernameLength)
-				.Select (length => ustring.Make ($"Username ({length} characters)"))
+				.Select (length => $"Username ({length} characters)")
 				.BindTo (usernameLengthLabel, x => x.Text)
 				.DisposeWith (_disposable);
 			Add (usernameLengthLabel);
@@ -100,7 +100,7 @@ namespace ReactiveExample {
 			};
 			ViewModel
 				.WhenAnyValue (x => x.PasswordLength)
-				.Select (length => ustring.Make ($"Password ({length} characters)"))
+				.Select (length => $"Password ({length} characters)")
 				.BindTo (passwordLengthLabel, x => x.Text)
 				.DisposeWith (_disposable);
 			Add (passwordLengthLabel);
@@ -108,8 +108,8 @@ namespace ReactiveExample {
 		}
 
 		Label ValidationLabel (View previous) {
-			var error = ustring.Make("Please, enter user name and password.");
-			var success = ustring.Make("The input is valid!");
+			var error = "Please, enter user name and password.";
+			var success = "The input is valid!";
 			var validationLabel = new Label(error) {
 				X = Pos.Left(previous),
 				Y = Pos.Top(previous) + 1,
@@ -130,8 +130,8 @@ namespace ReactiveExample {
 		}
 
 		Label LoginProgressLabel (View previous) {
-			var progress = ustring.Make ("Logging in...");
-			var idle = ustring.Make ("Press 'Login' to log in.");
+			var progress = "Logging in...";
+			var idle = "Press 'Login' to log in.";
 			var loginProgressLabel = new Label(idle) {
 				X = Pos.Left(previous),
 				Y = Pos.Top(previous) + 1,

+ 7 - 7
ReactiveExample/LoginViewModel.cs

@@ -3,7 +3,7 @@ using System.Reactive;
 using System.Reactive.Linq;
 using System.Runtime.Serialization;
 using System.Threading.Tasks;
-using NStack;
+using System.Text;
 using ReactiveUI;
 using ReactiveUI.Fody.Helpers;
 
@@ -30,8 +30,8 @@ namespace ReactiveExample {
 				x => x.Username, 
 				x => x.Password,
 				(username, password) =>
-					!ustring.IsNullOrEmpty (username) &&
-					!ustring.IsNullOrEmpty (password));
+					!string.IsNullOrEmpty (username) &&
+					!string.IsNullOrEmpty (password));
 			
 			_isValid = canLogin.ToProperty (this, x => x.IsValid);
 			Login = ReactiveCommand.CreateFromTask (
@@ -49,16 +49,16 @@ namespace ReactiveExample {
 			
 			Clear = ReactiveCommand.Create (() => { });
 			Clear.Subscribe (unit => {
-				Username = ustring.Empty;
-				Password = ustring.Empty;
+				Username = string.Empty;
+				Password = string.Empty;
 			});
 		}
 		
 		[Reactive, DataMember]
-		public ustring Username { get; set; } = ustring.Empty;
+		public string Username { get; set; } = string.Empty;
 		
 		[Reactive, DataMember]
-		public ustring Password { get; set; } = ustring.Empty;
+		public string Password { get; set; } = string.Empty;
 		
 		[IgnoreDataMember]
 		public int UsernameLength => _usernameLength.Value;

+ 1 - 1
ReactiveExample/README.md

@@ -38,7 +38,7 @@ usernameInput
 	.BindTo (ViewModel, x => x.Username);
 ```
 
-If you combine `OneWay` and `OneWayToSource` data bindings, you get `TwoWay` data binding. Also be sure to use the `ustring` type instead of the `string` type. Invoking commands should be as simple as this:
+If you combine `OneWay` and `OneWayToSource` data bindings, you get `TwoWay` data binding. Also be sure to use the `string` type instead of the `string` type. Invoking commands should be as simple as this:
 ```cs
 // 'clearButton' is 'Button'
 clearButton

+ 25 - 96
Terminal.Gui/Application.cs

@@ -51,12 +51,11 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// If <see langword="true"/>, forces the use of the System.Console-based (see <see cref="NetDriver"/>) driver. The default is <see langword="false"/>.
 		/// </summary>
-		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
 		public static bool UseSystemConsole { get; set; } = false;
 
 		// For Unit testing - ignores UseSystemConsole
 		internal static bool _forceFakeConsole;
-		
+
 		private static bool? _enableConsoleScrolling;
 		/// <summary>
 		/// The current <see cref="ConsoleDriver.EnableConsoleScrolling"/> used in the terminal.
@@ -119,7 +118,7 @@ namespace Terminal.Gui {
 			   File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename))
 			).ToList ();
 		}
-		
+
 		#region Initialization (Init/Shutdown)
 
 		/// <summary>
@@ -325,81 +324,6 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public static event EventHandler<ToplevelEventArgs> NotifyStopRunState;
 
-		/// <summary>
-		/// The execution state for a <see cref="Toplevel"/> view.
-		/// </summary>
-		public class RunState : IDisposable {
-			/// <summary>
-			/// Initializes a new <see cref="RunState"/> class.
-			/// </summary>
-			/// <param name="view"></param>
-			public RunState (Toplevel view)
-			{
-				Toplevel = view;
-			}
-			/// <summary>
-			/// The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.
-			/// </summary>
-			public Toplevel Toplevel { get; internal set; }
-
-#if DEBUG_IDISPOSABLE
-			/// <summary>
-			/// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
-			/// </summary>
-			public bool WasDisposed = false;
-
-			/// <summary>
-			/// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
-			/// </summary>
-			public int DisposedCount = 0;
-
-			/// <summary>
-			/// For debug (see DEBUG_IDISPOSABLE define) purposes; the runstate instances that have been created
-			/// </summary>
-			public static List<RunState> Instances = new List<RunState> ();
-			
-			/// <summary>
-			/// Creates a new RunState object.
-			/// </summary>
-			public RunState ()
-			{
-				Instances.Add (this);
-			}
-#endif
-
-			/// <summary>
-			/// Releases all resource used by the <see cref="Application.RunState"/> object.
-			/// </summary>
-			/// <remarks>
-			/// Call <see cref="Dispose()"/> when you are finished using the <see cref="Application.RunState"/>. 
-			/// </remarks>
-			/// <remarks>
-			/// <see cref="Dispose()"/> method leaves the <see cref="Application.RunState"/> in an unusable state. After
-			/// calling <see cref="Dispose()"/>, you must release all references to the
-			/// <see cref="Application.RunState"/> so the garbage collector can reclaim the memory that the
-			/// <see cref="Application.RunState"/> was occupying.
-			/// </remarks>
-			public void Dispose ()
-			{
-				Dispose (true);
-				GC.SuppressFinalize (this);
-#if DEBUG_IDISPOSABLE
-				WasDisposed = true;
-#endif
-			}
-
-			/// <summary>
-			/// Releases all resource used by the <see cref="Application.RunState"/> object.
-			/// </summary>
-			/// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
-			protected virtual void Dispose (bool disposing)
-			{
-				if (Toplevel != null && disposing) {
-					throw new InvalidOperationException ("You must clean up (Dispose) the Toplevel before calling Application.RunState.Dispose");
-				}
-			}
-		}
-
 		/// <summary>
 		/// Building block API: Prepares the provided <see cref="Toplevel"/> for execution.
 		/// </summary>
@@ -421,6 +345,9 @@ namespace Terminal.Gui {
 				throw new InvalidOperationException ("Only one Overlapped Container is allowed.");
 			}
 
+			// Ensure the mouse is ungrabed.
+			_mouseGrabView = null;
+
 			var rs = new RunState (Toplevel);
 
 			// View implements ISupportInitializeNotification which is derived from ISupportInitialize
@@ -439,10 +366,10 @@ namespace Terminal.Gui {
 				} else if (Top != null && Toplevel != Top && _toplevels.Contains (Top)) {
 					Top.OnLeave (Toplevel);
 				}
-				if (string.IsNullOrEmpty (Toplevel.Id.ToString ())) {
+				if (string.IsNullOrEmpty (Toplevel.Id)) {
 					var count = 1;
 					var id = (_toplevels.Count + count).ToString ();
-					while (_toplevels.Count > 0 && _toplevels.FirstOrDefault (x => x.Id.ToString () == id) != null) {
+					while (_toplevels.Count > 0 && _toplevels.FirstOrDefault (x => x.Id == id) != null) {
 						count++;
 						id = (_toplevels.Count + count).ToString ();
 					}
@@ -450,7 +377,7 @@ namespace Terminal.Gui {
 
 					_toplevels.Push (Toplevel);
 				} else {
-					var dup = _toplevels.FirstOrDefault (x => x.Id.ToString () == Toplevel.Id);
+					var dup = _toplevels.FirstOrDefault (x => x.Id == Toplevel.Id);
 					if (dup == null) {
 						_toplevels.Push (Toplevel);
 					}
@@ -494,7 +421,7 @@ namespace Terminal.Gui {
 				OverlappedTop?.OnChildLoaded (Toplevel);
 				Toplevel.OnLoaded ();
 				Toplevel.SetNeedsDisplay ();
-				Toplevel.Redraw (Toplevel.Bounds);
+				Toplevel.Draw ();
 				Toplevel.PositionCursor ();
 				Driver.Refresh ();
 			}
@@ -618,7 +545,7 @@ namespace Terminal.Gui {
 				}
 #endif
 			}
-		}		
+		}
 
 		/// <summary>
 		/// Triggers a refresh of the entire display.
@@ -630,7 +557,8 @@ namespace Terminal.Gui {
 			foreach (var v in _toplevels.Reverse ()) {
 				if (v.Visible) {
 					v.SetNeedsDisplay ();
-					v.Redraw (v.Bounds);
+					v.SetSubViewNeedsDisplay ();
+					v.Draw ();
 				}
 				last = v;
 			}
@@ -763,28 +691,29 @@ namespace Terminal.Gui {
 			firstIteration = false;
 
 			if (state.Toplevel != Top
-				&& (!Top._needsDisplay.IsEmpty || Top._childNeedsDisplay || Top.LayoutNeeded)) {
+				&& (!Top._needsDisplay.IsEmpty || Top._subViewNeedsDisplay || Top.LayoutNeeded)) {
 				state.Toplevel.SetNeedsDisplay (state.Toplevel.Bounds);
-				Top.Redraw (Top.Bounds);
+				Top.Draw ();
 				foreach (var top in _toplevels.Reverse ()) {
 					if (top != Top && top != state.Toplevel) {
 						top.SetNeedsDisplay ();
-						top.Redraw (top.Bounds);
+						top.SetSubViewNeedsDisplay ();
+						top.Draw ();
 					}
 				}
 			}
 			if (_toplevels.Count == 1 && state.Toplevel == Top
 				&& (Driver.Cols != state.Toplevel.Frame.Width || Driver.Rows != state.Toplevel.Frame.Height)
-				&& (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._childNeedsDisplay || state.Toplevel.LayoutNeeded)) {
+				&& (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._subViewNeedsDisplay || state.Toplevel.LayoutNeeded)) {
 
 				Driver.SetAttribute (Colors.TopLevel.Normal);
 				state.Toplevel.Clear (new Rect (0, 0, Driver.Cols, Driver.Rows));
 
 			}
 
-			if (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._childNeedsDisplay || state.Toplevel.LayoutNeeded
+			if (!state.Toplevel._needsDisplay.IsEmpty || state.Toplevel._subViewNeedsDisplay || state.Toplevel.LayoutNeeded
 				|| OverlappedChildNeedsDisplay ()) {
-				state.Toplevel.Redraw (state.Toplevel.Bounds);
+				state.Toplevel.Draw ();
 				//if (state.Toplevel.SuperView != null) {
 				//	state.Toplevel.SuperView?.OnRenderLineCanvas ();
 				//} else {
@@ -796,8 +725,8 @@ namespace Terminal.Gui {
 				Driver.UpdateCursor ();
 			}
 			if (state.Toplevel != Top && !state.Toplevel.Modal
-				&& (!Top._needsDisplay.IsEmpty || Top._childNeedsDisplay || Top.LayoutNeeded)) {
-				Top.Redraw (Top.Bounds);
+				&& (!Top._needsDisplay.IsEmpty || Top._subViewNeedsDisplay || Top.LayoutNeeded)) {
+				Top.Draw ();
 			}
 		}
 
@@ -892,7 +821,7 @@ namespace Terminal.Gui {
 				NotifyStopRunState?.Invoke (top, new ToplevelEventArgs (top));
 			}
 		}
-		
+
 		/// <summary>
 		/// Building block API: completes the execution of a <see cref="Toplevel"/> that was started with <see cref="Begin(Toplevel)"/> .
 		/// </summary>
@@ -1203,9 +1132,9 @@ namespace Terminal.Gui {
 
 		static void ProcessMouseEvent (MouseEvent me)
 		{
-			bool OutsideFrame (Point p, Rect r)
+			bool OutsideBounds (Point p, Rect r)
 			{
-				return p.X < 0 || p.X > r.Width - 1 || p.Y < 0 || p.Y > r.Height - 1;
+				return p.X < 0 || p.X > r.Right || p.Y < 0 || p.Y > r.Bottom;
 			}
 
 			if (IsMouseDisabled) {
@@ -1238,7 +1167,7 @@ namespace Terminal.Gui {
 					OfY = me.Y - newxy.Y,
 					View = view
 				};
-				if (OutsideFrame (new Point (nme.X, nme.Y), _mouseGrabView.Frame)) {
+				if (OutsideBounds (new Point (nme.X, nme.Y), _mouseGrabView.Bounds)) {
 					_lastMouseOwnerView?.OnMouseLeave (me);
 				}
 				//System.Diagnostics.Debug.WriteLine ($"{nme.Flags};{nme.X};{nme.Y};{mouseGrabView}");

+ 12 - 8
Terminal.Gui/Clipboard/Clipboard.cs

@@ -1,4 +1,4 @@
-using NStack;
+using System.Text;
 using System;
 
 namespace Terminal.Gui {
@@ -25,22 +25,26 @@ namespace Terminal.Gui {
 	/// </para>
 	/// </remarks>
 	public static class Clipboard {
-		static ustring contents;
+		static string contents;
 
 		/// <summary>
 		/// Gets (copies from) or sets (pastes to) the contents of the OS clipboard.
 		/// </summary>
-		public static ustring Contents {
+		public static string Contents {
 			get {
 				try {
 					if (IsSupported) {
-						return contents = ustring.Make (Application.Driver.Clipboard.GetClipboardData ());
-					} else {
-						return contents;
+						var clipData = Application.Driver.Clipboard.GetClipboardData ();
+						if (clipData == null) {
+							// throw new InvalidOperationException ($"{Application.Driver.GetType ().Name}.GetClipboardData returned null instead of string.Empty");
+							clipData = string.Empty;
+						}
+						contents = clipData;
 					}
 				} catch (Exception) {
-					return contents;
+					contents = string.Empty;
 				}
+				return contents;
 			}
 			set {
 				try {
@@ -48,7 +52,7 @@ namespace Terminal.Gui {
 						if (value == null) {
 							value = string.Empty;
 						}
-						Application.Driver.Clipboard.SetClipboardData (value.ToString ());
+						Application.Driver.Clipboard.SetClipboardData (value);
 					}
 					contents = value;
 				} catch (NotSupportedException e) {

+ 29 - 32
Terminal.Gui/Configuration/AppScope.cs

@@ -9,37 +9,34 @@ using static Terminal.Gui.ConfigurationManager;
 
 #nullable enable
 
-namespace Terminal.Gui {
+namespace Terminal.Gui; 
 
-	public static partial class ConfigurationManager {
-		/// <summary>
-		/// The <see cref="Scope{T}"/> class for application-defined configuration settings.
-		/// </summary>
-		/// <remarks>
-		/// </remarks>
-		/// <example>
-		/// <para>
-		/// Use the <see cref="SerializableConfigurationProperty"/> attribute to mark properties that should be serialized as part
-		/// of application-defined configuration settings.
-		/// </para>
-		/// <code>
-		/// public class MyAppSettings {
-		///	[SerializableConfigurationProperty (Scope = typeof (AppScope))]
-		///	public static bool? MyProperty { get; set; } = true;
-		/// }
-		/// </code>
-		/// <para>
-		/// THe resultant Json will look like this:
-		/// </para>
-		/// <code>
-		///   "AppSettings": {
-		///     "MyAppSettings.MyProperty": true,
-		///     "UICatalog.ShowStatusBar": true
-		///   },
-		/// </code>
-		/// </example> 
-		[JsonConverter (typeof (ScopeJsonConverter<AppScope>))]
-		public class AppScope : Scope<AppScope> {
-		}
-	}
+/// <summary>
+/// The <see cref="Scope{T}"/> class for application-defined configuration settings.
+/// </summary>
+/// <remarks>
+/// </remarks>
+/// <example>
+/// <para>
+/// Use the <see cref="SerializableConfigurationProperty"/> attribute to mark properties that should be serialized as part
+/// of application-defined configuration settings.
+/// </para>
+/// <code>
+/// public class MyAppSettings {
+///	[SerializableConfigurationProperty (Scope = typeof (AppScope))]
+///	public static bool? MyProperty { get; set; } = true;
+/// }
+/// </code>
+/// <para>
+/// THe resultant Json will look like this:
+/// </para>
+/// <code>
+///   "AppSettings": {
+///     "MyAppSettings.MyProperty": true,
+///     "UICatalog.ShowStatusBar": true
+///   },
+/// </code>
+/// </example> 
+[JsonConverter (typeof (ScopeJsonConverter<AppScope>))]
+public class AppScope : Scope<AppScope> {
 }

+ 1 - 3
Terminal.Gui/Configuration/AttributeJsonConverter.cs

@@ -7,7 +7,7 @@ namespace Terminal.Gui {
 	/// <summary>
 	/// Json converter fro the <see cref="Attribute"/> class.
 	/// </summary>
-	public class AttributeJsonConverter : JsonConverter<Attribute> {
+	class AttributeJsonConverter : JsonConverter<Attribute> {
 		private static AttributeJsonConverter instance;
 
 		/// <summary>
@@ -23,7 +23,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <inheritdoc/>
 		public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 		{
 			if (reader.TokenType != JsonTokenType.StartObject) {
@@ -74,7 +73,6 @@ namespace Terminal.Gui {
 			throw new JsonException ();
 		}
 
-		/// <inheritdoc/>
 		public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options)
 		{
 			writer.WriteStartObject ();

+ 1 - 3
Terminal.Gui/Configuration/ColorJsonConverter.cs

@@ -7,7 +7,7 @@ namespace Terminal.Gui {
 	/// <summary>
 	/// Json converter for the <see cref="Color"/> class.
 	/// </summary>
-	public class ColorJsonConverter : JsonConverter<Color> {
+	internal class ColorJsonConverter : JsonConverter<Color> {
 		private static ColorJsonConverter instance;
 
 		/// <summary>
@@ -23,7 +23,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <inheritdoc/>
 		public override Color Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 		{
 			// Check if the value is a string
@@ -52,7 +51,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <inheritdoc/>
 		public override void Write (Utf8JsonWriter writer, Color value, JsonSerializerOptions options)
 		{
 			// Try to get the human readable color name from the map

+ 1 - 1
Terminal.Gui/Configuration/ColorSchemeJsonConverter.cs

@@ -6,7 +6,7 @@ namespace Terminal.Gui {
 	/// <summary>
 	/// Implements a JSON converter for <see cref="ColorScheme"/>. 
 	/// </summary>
-	public class ColorSchemeJsonConverter : JsonConverter<ColorScheme> {
+	class ColorSchemeJsonConverter : JsonConverter<ColorScheme> {
 		private static ColorSchemeJsonConverter instance;
 
 		/// <summary>

+ 97 - 0
Terminal.Gui/Configuration/ConfigProperty.cs

@@ -0,0 +1,97 @@
+using System;
+using System.Reflection;
+using System.Text.Json.Serialization;
+
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> 
+/// to get and set the property's value.
+/// </summary>
+/// <remarks>
+/// Configuration properties must be <see langword="public"/> and <see langword="static"/> 
+/// and have the <see cref="SerializableConfigurationProperty"/>
+/// attribute. If the type of the property requires specialized JSON serialization, 
+/// a <see cref="JsonConverter"/> must be provided using 
+/// the <see cref="JsonConverterAttribute"/> attribute.
+/// </remarks>
+public class ConfigProperty {
+	private object? propertyValue;
+
+	/// <summary>
+	/// Describes the property.
+	/// </summary>
+	public PropertyInfo? PropertyInfo { get; set; }
+
+	/// <summary>
+	/// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
+	/// or the actual property name.
+	/// </summary>
+	/// <param name="pi"></param>
+	/// <returns></returns>
+	public static string GetJsonPropertyName (PropertyInfo pi)
+	{
+		var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
+		return jpna?.Name ?? pi.Name;
+	}
+
+	/// <summary>
+	/// Holds the property's value as it was either read from the class's implementation or from a config file. 
+	/// If the property has not been set (e.g. because no configuration file specified a value), 
+	/// this will be <see langword="null"/>.
+	/// </summary>
+	/// <remarks>
+	/// On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements of 
+	/// the object that are non-null).
+	/// </remarks>
+	public object? PropertyValue {
+		get => propertyValue;
+		set {
+			propertyValue = value;
+		}
+	}
+
+	internal object? UpdateValueFrom (object source)
+	{
+		if (source == null) {
+			return PropertyValue;
+		}
+
+		var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
+		if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) {
+			throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
+		}
+		if (PropertyValue != null && source != null) {
+			PropertyValue = ConfigurationManager.DeepMemberwiseCopy (source, PropertyValue);
+		} else {
+			PropertyValue = source;
+		}
+
+		return PropertyValue;
+	}
+
+	/// <summary>
+	/// Retrieves (using reflection) the value of the static property described in <see cref="PropertyInfo"/>
+	/// into <see cref="PropertyValue"/>.
+	/// </summary>
+	/// <returns></returns>
+	public object? RetrieveValue ()
+	{
+		return PropertyValue = PropertyInfo!.GetValue (null);
+	}
+
+	/// <summary>
+	/// Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.
+	/// </summary>
+	/// <returns></returns>
+	public bool Apply ()
+	{
+		if (PropertyValue != null) {
+			PropertyInfo?.SetValue (null, ConfigurationManager.DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
+		}
+		return PropertyValue != null;
+	}
+
+}

+ 460 - 564
Terminal.Gui/Configuration/ConfigurationManager.cs

@@ -1,4 +1,6 @@
-using System;
+global using CM = Terminal.Gui.ConfigurationManager;
+
+using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Diagnostics;
@@ -8,642 +10,536 @@ using System.Reflection;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json.Serialization;
-using System.Threading.Tasks;
-using static Terminal.Gui.ConfigurationManager;
 
 #nullable enable
 
-namespace Terminal.Gui {
-	/// <summary>
-	/// Provides settings and configuration management for Terminal.Gui applications. 
-	/// <para>
-	/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files.
-	/// The configuration files can be placed in at <c>.tui</c> folder in the user's home directory (e.g. <c>C:/Users/username/.tui</c>, 
-	/// or <c>/usr/username/.tui</c>),
-	/// the folder where the Terminal.Gui application was launched from (e.g. <c>./.tui</c>), or as a resource
-	/// within the Terminal.Gui application's main assembly. 
-	/// </para>
-	/// <para>
-	/// Settings are defined in JSON format, according to this schema: 
-	///	https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
-	/// </para>
-	/// <para>
-	/// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>. Settings 
-	/// that will apply to a specific Terminal.Gui application reside in files named <c>appname.config.json</c>,
-	/// where <c>appname</c> is the assembly name of the application (e.g. <c>UICatalog.config.json</c>).
-	/// </para>
-	/// Settings are applied using the following precedence (higher precedence settings
-	/// overwrite lower precedence settings):
-	/// <para>
-	///	1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) -- Highest precedence 
-	/// </para>
-	/// <para>
-	///	2. Application configuration found in the directory the app was launched from (<c>./.tui/appname.config.json</c>).
-	/// </para>
-	/// <para>
-	///	3. Application configuration found in the applications's resources (<c>Resources/config.json</c>). 
-	/// </para>
-	/// <para>
-	///	4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).
-	/// </para>
-	/// <para>
-	///	5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).
-	/// </para>
-	/// <para>
-	///     6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
-	/// </para>
-	/// </summary>
-	public static partial class ConfigurationManager {
-
-		private static readonly string _configFilename = "config.json";
-
-		private static readonly JsonSerializerOptions serializerOptions = new JsonSerializerOptions {
-			ReadCommentHandling = JsonCommentHandling.Skip,
-			PropertyNameCaseInsensitive = true,
-			DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
-			WriteIndented = true,
-			Converters = {
-				// No need to set converters - the ConfigRootConverter uses property attributes apply the correct
-				// Converter.
+namespace Terminal.Gui;
+/// <summary>
+/// Provides settings and configuration management for Terminal.Gui applications. 
+/// <para>
+/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files.
+/// The configuration files can be placed in at <c>.tui</c> folder in the user's home directory (e.g. <c>C:/Users/username/.tui</c>, 
+/// or <c>/usr/username/.tui</c>),
+/// the folder where the Terminal.Gui application was launched from (e.g. <c>./.tui</c>), or as a resource
+/// within the Terminal.Gui application's main assembly. 
+/// </para>
+/// <para>
+/// Settings are defined in JSON format, according to this schema: 
+///	https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json
+/// </para>
+/// <para>
+/// Settings that will apply to all applications (global settings) reside in files named <c>config.json</c>. Settings 
+/// that will apply to a specific Terminal.Gui application reside in files named <c>appname.config.json</c>,
+/// where <c>appname</c> is the assembly name of the application (e.g. <c>UICatalog.config.json</c>).
+/// </para>
+/// Settings are applied using the following precedence (higher precedence settings
+/// overwrite lower precedence settings):
+/// <para>
+///	1. Application configuration found in the users's home directory (<c>~/.tui/appname.config.json</c>) -- Highest precedence 
+/// </para>
+/// <para>
+///	2. Application configuration found in the directory the app was launched from (<c>./.tui/appname.config.json</c>).
+/// </para>
+/// <para>
+///	3. Application configuration found in the applications's resources (<c>Resources/config.json</c>). 
+/// </para>
+/// <para>
+///	4. Global configuration found in the user's home directory (<c>~/.tui/config.json</c>).
+/// </para>
+/// <para>
+///	5. Global configuration found in the directory the app was launched from (<c>./.tui/config.json</c>).
+/// </para>
+/// <para>
+///     6. Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
+/// </para>
+/// </summary>
+public static partial class ConfigurationManager {
+
+	private static readonly string _configFilename = "config.json";
+
+	internal static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions {
+		ReadCommentHandling = JsonCommentHandling.Skip,
+		PropertyNameCaseInsensitive = true,
+		DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+		WriteIndented = true,
+		Converters = {
+				// We override the standard Rune converter to support specifying Glyphs in
+				// a flexible way
+				new RuneJsonConverter(),
 			},
-		};
+	};
 
-		/// <summary>
-		/// An attribute that can be applied to a property to indicate that it should included in the configuration file.
-		/// </summary>
-		/// <example>
-		/// 	[SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
-		///	public static LineStyle DefaultBorderStyle {
-		///	...
-		/// </example>
-		[AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
-		public class SerializableConfigurationProperty : System.Attribute {
-			/// <summary>
-			/// Specifies the scope of the property. 
-			/// </summary>
-			public Type? Scope { get; set; }
-
-			/// <summary>
-			/// If <see langword="true"/>, the property will be serialized to the configuration file using only the property name
-			/// as the key. If <see langword="false"/>, the property will be serialized to the configuration file using the
-			/// property name pre-pended with the classname (e.g. <c>Application.UseSystemConsole</c>).
-			/// </summary>
-			public bool OmitClassName { get; set; }
-		}
+	/// <summary>
+	/// A dictionary of all properties in the Terminal.Gui project that are decorated with the <see cref="SerializableConfigurationProperty"/> attribute.
+	/// The keys are the property names pre-pended with the class that implements the property (e.g. <c>Application.UseSystemConsole</c>).
+	/// The values are instances of <see cref="ConfigProperty"/> which hold the property's value and the
+	/// <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to get and set the property's value.
+	/// </summary>
+	/// <remarks>
+	/// Is <see langword="null"/> until <see cref="Initialize"/> is called. 
+	/// </remarks>
+	internal static Dictionary<string, ConfigProperty>? _allConfigProperties;
 
-		/// <summary>
-		/// Holds a property's value and the <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> 
-		/// to get and set the property's value.
-		/// </summary>
-		/// <remarks>
-		/// Configuration properties must be <see langword="public"/> and <see langword="static"/> 
-		/// and have the <see cref="SerializableConfigurationProperty"/>
-		/// attribute. If the type of the property requires specialized JSON serialization, 
-		/// a <see cref="JsonConverter"/> must be provided using 
-		/// the <see cref="JsonConverterAttribute"/> attribute.
-		/// </remarks>
-		public class ConfigProperty {
-			private object? propertyValue;
-
-			/// <summary>
-			/// Describes the property.
-			/// </summary>
-			public PropertyInfo? PropertyInfo { get; set; }
-
-			/// <summary>
-			/// Helper to get either the Json property named (specified by [JsonPropertyName(name)]
-			/// or the actual property name.
-			/// </summary>
-			/// <param name="pi"></param>
-			/// <returns></returns>
-			public static string GetJsonPropertyName (PropertyInfo pi)
-			{
-				var jpna = pi.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
-				return jpna?.Name ?? pi.Name;
-			}
+	/// <summary>
+	/// The backing property for <see cref="Settings"/>. 
+	/// </summary>
+	/// <remarks>
+	/// Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by
+	/// deserialization (see <see cref="Load"/>).
+	/// </remarks>
+	private static SettingsScope? _settings;
 
-			/// <summary>
-			/// Holds the property's value as it was either read from the class's implementation or from a config file. 
-			/// If the property has not been set (e.g. because no configuration file specified a value), 
-			/// this will be <see langword="null"/>.
-			/// </summary>
-			/// <remarks>
-			/// On <see langword="set"/>, performs a sparse-copy of the new value to the existing value (only copies elements of 
-			/// the object that are non-null).
-			/// </remarks>
-			public object? PropertyValue {
-				get => propertyValue;
-				set {
-					propertyValue = value;
-				}
+	/// <summary>
+	/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the <see cref="SettingsScope"/>
+	/// attribute value.
+	/// </summary>
+	public static SettingsScope? Settings {
+		get {
+			if (_settings == null) {
+				throw new InvalidOperationException ("ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property.");
 			}
+			return _settings;
+		}
+		set {
+			_settings = value!;
+		}
+	}
 
-			internal object? UpdateValueFrom (object source)
-			{
-				if (source == null) {
-					return PropertyValue;
-				}
+	/// <summary>
+	/// The root object of Terminal.Gui themes manager. Contains only properties with the <see cref="ThemeScope"/>
+	/// attribute value.
+	/// </summary>
+	public static ThemeManager? Themes => ThemeManager.Instance;
 
-				var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType);
-				if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) {
-					throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}.");
-				}
-				if (PropertyValue != null && source != null) {
-					PropertyValue = DeepMemberwiseCopy (source, PropertyValue);
-				} else {
-					PropertyValue = source;
-				}
+	/// <summary>
+	/// Application-specific configuration settings scope.
+	/// </summary>
+	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")]
+	public static AppScope? AppSettings { get; set; }
 
-				return PropertyValue;
-			}
+	/// <summary>
+	/// The set of glyphs used to draw checkboxes, lines, borders, etc...See also <seealso cref="Terminal.Gui.GlyphDefinitions"/>.
+	/// </summary>
+	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true),
+		JsonPropertyName ("Glyphs")]
+	public static GlyphDefinitions Glyphs { get; set; } = new GlyphDefinitions ();
 
-			/// <summary>
-			/// Retrieves (using reflection) the value of the static property described in <see cref="PropertyInfo"/>
-			/// into <see cref="PropertyValue"/>.
-			/// </summary>
-			/// <returns></returns>
-			public object? RetrieveValue ()
-			{
-				return PropertyValue = PropertyInfo!.GetValue (null);
-			}
+	/// <summary>
+	/// Initializes the internal state of ConfigurationManager. Nominally called once as part of application
+	/// startup to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
+	/// </summary>
+	internal static void Initialize ()
+	{
+		_allConfigProperties = new Dictionary<string, ConfigProperty> ();
+		_settings = null;
 
-			/// <summary>
-			/// Applies the <see cref="PropertyValue"/> to the property described by <see cref="PropertyInfo"/>.
-			/// </summary>
-			/// <returns></returns>
-			public bool Apply ()
-			{
-				if (PropertyValue != null) {
-					PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null)));
-				}
-				return PropertyValue != null;
-			}
-		}
+		Dictionary<string, Type> classesWithConfigProps = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase);
+		// Get Terminal.Gui.dll classes
 
-		/// <summary>
-		/// A dictionary of all properties in the Terminal.Gui project that are decorated with the <see cref="SerializableConfigurationProperty"/> attribute.
-		/// The keys are the property names pre-pended with the class that implements the property (e.g. <c>Application.UseSystemConsole</c>).
-		/// The values are instances of <see cref="ConfigProperty"/> which hold the property's value and the
-		/// <see cref="PropertyInfo"/> that allows <see cref="ConfigurationManager"/> to get and set the property's value.
-		/// </summary>
-		/// <remarks>
-		/// Is <see langword="null"/> until <see cref="Initialize"/> is called. 
-		/// </remarks>
-		private static Dictionary<string, ConfigProperty>? _allConfigProperties;
+		var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
+			    from type in assembly.GetTypes ()
+			    where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
+			    select type;
 
-		/// <summary>
-		/// The backing property for <see cref="Settings"/>. 
-		/// </summary>
-		/// <remarks>
-		/// Is <see langword="null"/> until <see cref="Reset"/> is called. Gets set to a new instance by
-		/// deserialization (see <see cref="Load"/>).
-		/// </remarks>
-		private static SettingsScope? _settings;
+		foreach (var classWithConfig in types) {
+			classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
+		}
 
-		/// <summary>
-		/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the <see cref="SettingsScope"/>
-		/// attribute value.
-		/// </summary>
-		public static SettingsScope? Settings {
-			get {
-				if (_settings == null) {
-					throw new InvalidOperationException ("ConfigurationManager has not been initialized. Call ConfigurationManager.Reset() before accessing the Settings property.");
+		Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} classes:");
+		classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($"  Class: {x.Key}"));
+
+		foreach (var p in from c in classesWithConfigProps
+				  let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
+					prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
+				  let enumerable = props
+				  from p in enumerable
+				  select p) {
+			if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
+				if (p.GetGetMethod (true)!.IsStatic) {
+					// If the class name is omitted, JsonPropertyName is allowed. 
+					_allConfigProperties!.Add (scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", new ConfigProperty {
+						PropertyInfo = p,
+						PropertyValue = null
+					});
+				} else {
+					throw new Exception ($"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static.");
 				}
-				return _settings;
-			}
-			set {
-				_settings = value!;
 			}
 		}
 
-		/// <summary>
-		/// The root object of Terminal.Gui themes manager. Contains only properties with the <see cref="ThemeScope"/>
-		/// attribute value.
-		/// </summary>
-		public static ThemeManager? Themes => ThemeManager.Instance;
+		_allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key).ToDictionary (x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase);
 
-		/// <summary>
-		/// Application-specific configuration settings scope.
-		/// </summary>
-		[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")]
-		public static AppScope? AppSettings { get; set; }
+		Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
+		//_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($"  Property: {x.Key}"));
 
-		/// <summary>
-		/// Initializes the internal state of ConfigurationManager. Nominally called once as part of application
-		/// startup to initialize global state. Also called from some Unit Tests to ensure correctness (e.g. Reset()).
-		/// </summary>
-		internal static void Initialize ()
-		{
-			_allConfigProperties = new Dictionary<string, ConfigProperty> ();
-			_settings = null;
-
-			Dictionary<string, Type> classesWithConfigProps = new Dictionary<string, Type> (StringComparer.InvariantCultureIgnoreCase);
-			// Get Terminal.Gui.dll classes
+		AppSettings = new AppScope ();
+	}
 
-			var types = from assembly in AppDomain.CurrentDomain.GetAssemblies ()
-				    from type in assembly.GetTypes ()
-				    where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null)
-				    select type;
+	/// <summary>
+	/// Creates a JSON document with the configuration specified. 
+	/// </summary>
+	/// <returns></returns>
+	internal static string ToJson ()
+	{
+		Debug.WriteLine ($"ConfigurationManager.ToJson()");
+		return JsonSerializer.Serialize<SettingsScope> (Settings!, _serializerOptions);
+	}
 
-			foreach (var classWithConfig in types) {
-				classesWithConfigProps.Add (classWithConfig.Name, classWithConfig);
-			}
+	internal static Stream ToStream ()
+	{
+		var json = JsonSerializer.Serialize<SettingsScope> (Settings!, _serializerOptions);
+		// turn it into a stream
+		var stream = new MemoryStream ();
+		var writer = new StreamWriter (stream);
+		writer.Write (json);
+		writer.Flush ();
+		stream.Position = 0;
+		return stream;
+	}
 
-			Debug.WriteLine ($"ConfigManager.getConfigProperties found {classesWithConfigProps.Count} clases:");
-			classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($"  Class: {x.Key}"));
-
-			foreach (var p in from c in classesWithConfigProps
-					  let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop =>
-						prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty)
-					  let enumerable = props
-					  from p in enumerable
-					  select p) {
-				if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) {
-					if (p.GetGetMethod (true)!.IsStatic) {
-						// If the class name is omitted, JsonPropertyName is allowed. 
-						_allConfigProperties!.Add (scp.OmitClassName ? ConfigProperty.GetJsonPropertyName (p) : $"{p.DeclaringType?.Name}.{p.Name}", new ConfigProperty {
-							PropertyInfo = p,
-							PropertyValue = null
-						});
-					} else {
-						throw new Exception ($"Property {p.Name} in class {p.DeclaringType?.Name} is not static. All SerializableConfigurationProperty properties must be static.");
-					}
-				}
-			}
+	/// <summary>
+	/// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters 
+	/// an error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the 
+	/// console when <see cref="Application.Shutdown"/> is called. 
+	/// </summary>
+	[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
+	public static bool? ThrowOnJsonErrors { get; set; } = false;
 
-			_allConfigProperties = _allConfigProperties!.OrderBy (x => x.Key).ToDictionary (x => x.Key, x => x.Value, StringComparer.InvariantCultureIgnoreCase);
+	internal static StringBuilder jsonErrors = new StringBuilder ();
 
-			Debug.WriteLine ($"ConfigManager.Initialize found {_allConfigProperties.Count} properties:");
-			_allConfigProperties.ToList ().ForEach (x => Debug.WriteLine ($"  Property: {x.Key}"));
+	internal static void AddJsonError (string error)
+	{
+		Debug.WriteLine ($"ConfigurationManager: {error}");
+		jsonErrors.AppendLine (error);
+	}
 
-			AppSettings = new AppScope ();
+	/// <summary>
+	/// Prints any Json deserialization errors that occurred during deserialization to the console.
+	/// </summary>
+	public static void PrintJsonErrors ()
+	{
+		if (jsonErrors.Length > 0) {
+			Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
+			Console.WriteLine (jsonErrors.ToString ());
 		}
+	}
 
-		/// <summary>
-		/// Creates a JSON document with the configuration specified. 
-		/// </summary>
-		/// <returns></returns>
-		internal static string ToJson ()
-		{
-			Debug.WriteLine ($"ConfigurationManager.ToJson()");
-			return JsonSerializer.Serialize<SettingsScope> (Settings!, serializerOptions);
-		}
+	private static void ClearJsonErrors ()
+	{
+		jsonErrors.Clear ();
+	}
+
+	/// <summary>
+	/// Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
+	/// event.
+	/// </summary>
+	public static void OnUpdated ()
+	{
+		Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+		Updated?.Invoke (null, new ConfigurationManagerEventArgs ());
+	}
 
-		internal static Stream ToStream ()
-		{
-			var json = JsonSerializer.Serialize<SettingsScope> (Settings!, serializerOptions);
-			// turn it into a stream
-			var stream = new MemoryStream ();
-			var writer = new StreamWriter (stream);
-			writer.Write (json);
-			writer.Flush ();
-			stream.Position = 0;
-			return stream;
+	/// <summary>
+	/// Event fired when the configuration has been updated from a configuration source.  
+	/// application.
+	/// </summary>
+	public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
+
+	/// <summary>
+	/// Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session
+	/// (e.g. in <see cref="Application.Init(ConsoleDriver, IMainLoopDriver)"/> starts. Called by <see cref="Load"/>
+	/// if the <c>reset</c> parameter is <see langword="true"/>.
+	/// </summary>
+	/// <remarks>
+	/// 
+	/// </remarks>
+	public static void Reset ()
+	{
+		Debug.WriteLine ($"ConfigurationManager.Reset()");
+		if (_allConfigProperties == null) {
+			ConfigurationManager.Initialize ();
 		}
 
-		/// <summary>
-		/// Gets or sets whether the <see cref="ConfigurationManager"/> should throw an exception if it encounters 
-		/// an error on deserialization. If <see langword="false"/> (the default), the error is logged and printed to the 
-		/// console when <see cref="Application.Shutdown"/> is called. 
-		/// </summary>
-		[SerializableConfigurationProperty (Scope = typeof (SettingsScope))]
-		public static bool? ThrowOnJsonErrors { get; set; } = false;
+		ClearJsonErrors ();
 
-		internal static StringBuilder jsonErrors = new StringBuilder ();
+		Settings = new SettingsScope ();
+		ThemeManager.Reset ();
+		AppSettings = new AppScope ();
 
-		private static void AddJsonError (string error)
-		{
-			Debug.WriteLine ($"ConfigurationManager: {error}");
-			jsonErrors.AppendLine (error);
+		// To enable some unit tests, we only load from resources if the flag is set
+		if (Locations.HasFlag (ConfigLocations.DefaultOnly)) {
+			Settings.UpdateFromResource (typeof (ConfigurationManager).Assembly, $"Terminal.Gui.Resources.{_configFilename}");
 		}
+		Apply ();
+		ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
+		AppSettings?.Apply ();
+	}
 
-		/// <summary>
-		/// Prints any Json deserialization errors that occurred during deserialization to the console.
-		/// </summary>
-		public static void PrintJsonErrors ()
-		{
-			if (jsonErrors.Length > 0) {
-				Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:");
-				Console.WriteLine (jsonErrors.ToString ());
-			}
+	/// <summary>
+	/// Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of
+	/// the library to generate the default configuration file. Before calling Application.Init, make sure
+	/// <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
+	/// </summary>
+	/// <remarks>
+	/// <para>
+	/// This method is only really useful when using ConfigurationManagerTests
+	/// to generate the JSON doc that is embedded into Terminal.Gui (during development). 
+	/// </para>
+	/// <para>
+	/// WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes)
+	/// that are NOT generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
+	/// make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
+	/// </para>		
+	/// </remarks>
+	internal static void GetHardCodedDefaults ()
+	{
+		if (_allConfigProperties == null) {
+			throw new InvalidOperationException ("Initialize must be called first.");
 		}
-
-		private static void ClearJsonErrors ()
-		{
-			jsonErrors.Clear ();
+		Settings = new SettingsScope ();
+		ThemeManager.GetHardCodedDefaults ();
+		AppSettings?.RetrieveValues ();
+		foreach (var p in Settings!.Where (cp => cp.Value.PropertyInfo != null)) {
+			Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
 		}
+	}
 
-		/// <summary>
-		/// Called when the configuration has been updated from a configuration file. Invokes the <see cref="Updated"/>
-		/// event.
-		/// </summary>
-		public static void OnUpdated ()
-		{
-			Debug.WriteLine ($"ConfigurationManager.OnApplied()");
-			Updated?.Invoke (null, new ConfigurationManagerEventArgs ());
+	/// <summary>
+	/// Applies the configuration settings to the running <see cref="Application"/> instance.
+	/// </summary>
+	public static void Apply ()
+	{
+		bool settings = Settings?.Apply () ?? false;
+		bool themes = !string.IsNullOrEmpty (ThemeManager.SelectedTheme) && (ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false);
+		bool appsettings = AppSettings?.Apply () ?? false;
+		if (settings || themes || appsettings) {
+			OnApplied ();
 		}
+	}
 
-		/// <summary>
-		/// Event fired when the configuration has been updated from a configuration source.  
-		/// application.
-		/// </summary>
-		public static event EventHandler<ConfigurationManagerEventArgs>? Updated;
-
-		/// <summary>
-		/// Resets the state of <see cref="ConfigurationManager"/>. Should be called whenever a new app session
-		/// (e.g. in <see cref="Application.Init(ConsoleDriver, IMainLoopDriver)"/> starts. Called by <see cref="Load"/>
-		/// if the <c>reset</c> parameter is <see langword="true"/>.
-		/// </summary>
-		/// <remarks>
-		/// 
-		/// </remarks>
-		public static void Reset ()
-		{
-			Debug.WriteLine ($"ConfigurationManager.Reset()");
-			if (_allConfigProperties == null) {
-				ConfigurationManager.Initialize ();
-			}
-
-			ClearJsonErrors ();
-
-			Settings = new SettingsScope ();
-			ThemeManager.Reset ();
-			AppSettings = new AppScope ();
+	/// <summary>
+	/// Called when an updated configuration has been applied to the  
+	/// application. Fires the <see cref="Applied"/> event.
+	/// </summary>
+	public static void OnApplied ()
+	{
+		Debug.WriteLine ($"ConfigurationManager.OnApplied()");
+		Applied?.Invoke (null, new ConfigurationManagerEventArgs ());
+	}
 
-			// To enable some unit tests, we only load from resources if the flag is set
-			if (Locations.HasFlag (ConfigLocations.DefaultOnly)) Settings.UpdateFromResource (typeof (ConfigurationManager).Assembly, $"Terminal.Gui.Resources.{_configFilename}");
+	/// <summary>
+	/// Event fired when an updated configuration has been applied to the  
+	/// application.
+	/// </summary>
+	public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
 
-			Apply ();
-			ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply ();
-			AppSettings?.Apply ();
-		}
+	/// <summary>
+	/// Name of the running application. By default this property is set to the application's assembly name.
+	/// </summary>
+	public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
 
+	/// <summary>
+	/// Describes the location of the configuration files. The constants can be
+	/// combined (bitwise) to specify multiple locations.
+	/// </summary>
+	[Flags]
+	public enum ConfigLocations {
 		/// <summary>
-		/// Retrieves the hard coded default settings from the Terminal.Gui library implementation. Used in development of
-		/// the library to generate the default configuration file. Before calling Application.Init, make sure
-		/// <see cref="Locations"/> is set to <see cref="ConfigLocations.None"/>.
+		/// No configuration will be loaded.
 		/// </summary>
 		/// <remarks>
-		/// <para>
-		/// This method is only really useful when using ConfigurationManagerTests
-		/// to generate the JSON doc that is embedded into Terminal.Gui (during development). 
-		/// </para>
-		/// <para>
-		/// WARNING: The <c>Terminal.Gui.Resources.config.json</c> resource has setting definitions (Themes)
-		/// that are NOT generated by this function. If you use this function to regenerate <c>Terminal.Gui.Resources.config.json</c>,
-		/// make sure you copy the Theme definitions from the existing <c>Terminal.Gui.Resources.config.json</c> file.
-		/// </para>		
+		///  Used for development and testing only. For Terminal,Gui to function properly, at least
+		///  <see cref="DefaultOnly"/> should be set.
 		/// </remarks>
-		internal static void GetHardCodedDefaults ()
-		{
-			if (_allConfigProperties == null) {
-				throw new InvalidOperationException ("Initialize must be called first.");
-			}
-			Settings = new SettingsScope ();
-			ThemeManager.GetHardCodedDefaults ();
-			AppSettings?.RetrieveValues ();
-			foreach (var p in Settings!.Where (cp => cp.Value.PropertyInfo != null)) {
-				Settings! [p.Key].PropertyValue = p.Value.PropertyInfo?.GetValue (null);
-			}
-		}
+		None = 0,
 
 		/// <summary>
-		/// Applies the configuration settings to the running <see cref="Application"/> instance.
+		/// Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
 		/// </summary>
-		public static void Apply ()
-		{
-			bool settings = Settings?.Apply () ?? false;
-			bool themes = ThemeManager.Themes? [ThemeManager.SelectedTheme]?.Apply () ?? false;
-			bool appsettings = AppSettings?.Apply () ?? false;
-			if (settings || themes || appsettings) {
-				OnApplied ();
-			}
-		}
+		DefaultOnly,
 
 		/// <summary>
-		/// Called when an updated configuration has been applied to the  
-		/// application. Fires the <see cref="Applied"/> event.
+		/// This constant is a combination of all locations
 		/// </summary>
-		public static void OnApplied ()
-		{
-			Debug.WriteLine ($"ConfigurationManager.OnApplied()");
-			Applied?.Invoke (null, new ConfigurationManagerEventArgs ());
-		}
+		All = -1
 
-		/// <summary>
-		/// Event fired when an updated configuration has been applied to the  
-		/// application.
-		/// </summary>
-		public static event EventHandler<ConfigurationManagerEventArgs>? Applied;
+	}
 
-		/// <summary>
-		/// Name of the running application. By default this property is set to the application's assembly name.
-		/// </summary>
-		public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!;
+	/// <summary>
+	/// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files.
+	/// The value is <see cref="ConfigLocations.All"/>.
+	/// </summary>
+	public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
 
-		/// <summary>
-		/// Describes the location of the configuration files. The constants can be
-		/// combined (bitwise) to specify multiple locations.
-		/// </summary>
-		[Flags]
-		public enum ConfigLocations {
-			/// <summary>
-			/// No configuration will be loaded.
-			/// </summary>
-			/// <remarks>
-			///  Used for development and testing only. For Terminal,Gui to function properly, at least
-			///  <see cref="DefaultOnly"/> should be set.
-			/// </remarks>
-			None = 0,
-
-			/// <summary>
-			/// Global configuration in <c>Terminal.Gui.dll</c>'s resources (<c>Terminal.Gui.Resources.config.json</c>) -- Lowest Precidence.
-			/// </summary>
-			DefaultOnly,
-
-			/// <summary>
-			/// This constant is a combination of all locations
-			/// </summary>
-			All = -1
+	/// <summary>
+	/// Loads all settings found in the various configuration storage locations to 
+	/// the <see cref="ConfigurationManager"/>. Optionally,
+	/// resets all settings attributed with <see cref="SerializableConfigurationProperty"/> to the defaults.
+	/// </summary>
+	/// <remarks>
+	/// Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
+	/// </remarks>
+	/// <param name="reset">If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will
+	/// be reset to the defaults.</param>
+	public static void Load (bool reset = false)
+	{
+		Debug.WriteLine ($"ConfigurationManager.Load()");
+
+		if (reset) Reset ();
+
+		// LibraryResources is always loaded by Reset
+		if (Locations == ConfigLocations.All) {
+			var embeddedStylesResourceName = Assembly.GetEntryAssembly ()?
+				.GetManifestResourceNames ().FirstOrDefault (x => x.EndsWith (_configFilename));
+			if (string.IsNullOrEmpty (embeddedStylesResourceName)) {
+				embeddedStylesResourceName = _configFilename;
+			}
 
+			Settings = Settings?
+				// Global current directory
+				.Update ($"./.tui/{_configFilename}")?
+				// Global home directory
+				.Update ($"~/.tui/{_configFilename}")?
+				// App resources
+				.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)?
+				// App current directory
+				.Update ($"./.tui/{AppName}.{_configFilename}")?
+				// App home directory
+				.Update ($"~/.tui/{AppName}.{_configFilename}");
 		}
+	}
 
-		/// <summary>
-		/// Gets and sets the locations where <see cref="ConfigurationManager"/> will look for config files.
-		/// The value is <see cref="ConfigLocations.All"/>.
-		/// </summary>
-		public static ConfigLocations Locations { get; set; } = ConfigLocations.All;
-
-		/// <summary>
-		/// Loads all settings found in the various configuration storage locations to 
-		/// the <see cref="ConfigurationManager"/>. Optionally,
-		/// resets all settings attributed with <see cref="SerializableConfigurationProperty"/> to the defaults.
-		/// </summary>
-		/// <remarks>
-		/// Use <see cref="Apply"/> to cause the loaded settings to be applied to the running application.
-		/// </remarks>
-		/// <param name="reset">If <see langword="true"/> the state of <see cref="ConfigurationManager"/> will
-		/// be reset to the defaults.</param>
-		public static void Load (bool reset = false)
-		{
-			Debug.WriteLine ($"ConfigurationManager.Load()");
-
-			if (reset) Reset ();
-
-			// LibraryResources is always loaded by Reset
-			if (Locations == ConfigLocations.All) {
-				var embeddedStylesResourceName = Assembly.GetEntryAssembly ()?
-					.GetManifestResourceNames ().FirstOrDefault (x => x.EndsWith (_configFilename));
-				if (string.IsNullOrEmpty (embeddedStylesResourceName)) {
-					embeddedStylesResourceName = _configFilename;
-				}
+	/// <summary>
+	/// Returns an empty Json document with just the $schema tag.
+	/// </summary>
+	/// <returns></returns>
+	public static string GetEmptyJson ()
+	{
+		var emptyScope = new SettingsScope ();
+		emptyScope.Clear ();
+		return JsonSerializer.Serialize<SettingsScope> (emptyScope, _serializerOptions);
+	}
 
-				Settings = Settings?
-					// Global current directory
-					.Update ($"./.tui/{_configFilename}")?
-					// Global home directory
-					.Update ($"~/.tui/{_configFilename}")?
-					// App resources
-					.UpdateFromResource (Assembly.GetEntryAssembly ()!, embeddedStylesResourceName!)?
-					// App current directory
-					.Update ($"./.tui/{AppName}.{_configFilename}")?
-					// App home directory
-					.Update ($"~/.tui/{AppName}.{_configFilename}");
-			}
+	/// <summary>
+	/// System.Text.Json does not support copying a deserialized object to an existing instance.
+	/// To work around this, we implement a 'deep, memberwise copy' method. 
+	/// </summary>
+	/// <remarks>
+	/// TOOD: When System.Text.Json implements `PopulateObject` revisit
+	///	https://github.com/dotnet/corefx/issues/37627
+	/// </remarks>
+	/// <param name="source"></param>
+	/// <param name="destination"></param>
+	/// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
+	internal static object? DeepMemberwiseCopy (object? source, object? destination)
+	{
+		if (destination == null) {
+			throw new ArgumentNullException (nameof (destination));
 		}
 
-		/// <summary>
-		/// Returns an empty Json document with just the $schema tag.
-		/// </summary>
-		/// <returns></returns>
-		public static string GetEmptyJson ()
-		{
-			var emptyScope = new SettingsScope ();
-			emptyScope.Clear ();
-			return JsonSerializer.Serialize<SettingsScope> (emptyScope, serializerOptions);
+		if (source == null) {
+			return null!;
 		}
 
-		/// <summary>
-		/// System.Text.Json does not support copying a deserialized object to an existing instance.
-		/// To work around this, we implement a 'deep, memberwise copy' method. 
-		/// </summary>
-		/// <remarks>
-		/// TOOD: When System.Text.Json implements `PopulateObject` revisit
-		///	https://github.com/dotnet/corefx/issues/37627
-		/// </remarks>
-		/// <param name="source"></param>
-		/// <param name="destination"></param>
-		/// <returns><paramref name="destination"/> updated from <paramref name="source"/></returns>
-		internal static object? DeepMemberwiseCopy (object? source, object? destination)
-		{
-			if (destination == null) {
-				throw new ArgumentNullException (nameof (destination));
-			}
-
-			if (source == null) {
-				return null!;
-			}
-
-			if (source.GetType () == typeof (SettingsScope)) {
-				return ((SettingsScope)destination).Update ((SettingsScope)source);
-			}
-			if (source.GetType () == typeof (ThemeScope)) {
-				return ((ThemeScope)destination).Update ((ThemeScope)source);
-			}
-			if (source.GetType () == typeof (AppScope)) {
-				return ((AppScope)destination).Update ((AppScope)source);
-			}
+		if (source.GetType () == typeof (SettingsScope)) {
+			return ((SettingsScope)destination).Update ((SettingsScope)source);
+		}
+		if (source.GetType () == typeof (ThemeScope)) {
+			return ((ThemeScope)destination).Update ((ThemeScope)source);
+		}
+		if (source.GetType () == typeof (AppScope)) {
+			return ((AppScope)destination).Update ((AppScope)source);
+		}
 
-			// If value type, just use copy constructor.
-			if (source.GetType ().IsValueType || source.GetType () == typeof (string)) {
-				return source;
-			}
+		// If value type, just use copy constructor.
+		if (source.GetType ().IsValueType || source.GetType () == typeof (string)) {
+			return source;
+		}
 
-			// Dictionary
-			if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
-				foreach (var srcKey in ((IDictionary)source).Keys) {
-					if (((IDictionary)destination).Contains (srcKey))
-						((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
-					else {
-						((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
-					}
+		// Dictionary
+		if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) {
+			foreach (var srcKey in ((IDictionary)source).Keys) {
+				if (((IDictionary)destination).Contains (srcKey))
+					((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]);
+				else {
+					((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]);
 				}
-				return destination;
 			}
+			return destination;
+		}
 
-			// ALl other object types
-			var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
-			var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
-			foreach (var (sourceProp, destProp) in
-				from sourceProp in sourceProps
-				where destProps.Any (x => x.Name == sourceProp.Name)
-				let destProp = destProps.First (x => x.Name == sourceProp.Name)
-				where destProp.CanWrite
-				select (sourceProp, destProp)) {
-
-				var sourceVal = sourceProp.GetValue (source);
-				var destVal = destProp.GetValue (destination);
-				if (sourceVal != null) {
-					if (destVal != null) {
-						// Recurse
-						destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
-					} else {
-						destProp.SetValue (destination, sourceVal);
-					}
+		// ALl other object types
+		var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList ();
+		var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!;
+		foreach (var (sourceProp, destProp) in
+			from sourceProp in sourceProps
+			where destProps.Any (x => x.Name == sourceProp.Name)
+			let destProp = destProps.First (x => x.Name == sourceProp.Name)
+			where destProp.CanWrite
+			select (sourceProp, destProp)) {
+
+			var sourceVal = sourceProp.GetValue (source);
+			var destVal = destProp.GetValue (destination);
+			if (sourceVal != null) {
+				if (destVal != null) {
+					// Recurse
+					destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal));
+				} else {
+					destProp.SetValue (destination, sourceVal);
 				}
 			}
-			return destination!;
 		}
-
-		//public class ConfiguraitonLocation
-		//{
-		//	public string Name { get; set; } = string.Empty;
-
-		//	public string? Path { get; set; }
-
-		//	public async Task<SettingsScope> UpdateAsync (Stream stream)
-		//	{
-		//		var scope = await JsonSerializer.DeserializeAsync<SettingsScope> (stream, serializerOptions);
-		//		if (scope != null) {
-		//			ConfigurationManager.Settings?.UpdateFrom (scope);
-		//			return scope;
-		//		}
-		//		return new SettingsScope ();
-		//	}
-
-		//}
-
-		//public class StreamConfiguration {
-		//	private bool _reset;
-
-		//	public StreamConfiguration (bool reset)
-		//	{
-		//		_reset = reset;
-		//	}
-
-		//	public StreamConfiguration UpdateAppResources ()
-		//	{
-		//		if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources ();
-		//		return this;
-		//	}
-
-		//	public StreamConfiguration UpdateAppDirectory ()
-		//	{
-		//		if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory ();
-		//		return this;
-		//	}
-
-		//	// Additional update methods for each location here
-
-		//	private void LoadAppResources ()
-		//	{
-		//		// Load AppResources logic here
-		//	}
-
-		//	private void LoadAppDirectory ()
-		//	{
-		//		// Load AppDirectory logic here
-		//	}
-		//}
+		return destination!;
 	}
+
+	//public class ConfiguraitonLocation
+	//{
+	//	public string Name { get; set; } = string.Empty;
+
+	//	public string? Path { get; set; }
+
+	//	public async Task<SettingsScope> UpdateAsync (Stream stream)
+	//	{
+	//		var scope = await JsonSerializer.DeserializeAsync<SettingsScope> (stream, serializerOptions);
+	//		if (scope != null) {
+	//			ConfigurationManager.Settings?.UpdateFrom (scope);
+	//			return scope;
+	//		}
+	//		return new SettingsScope ();
+	//	}
+
+	//}
+
+	//public class StreamConfiguration {
+	//	private bool _reset;
+
+	//	public StreamConfiguration (bool reset)
+	//	{
+	//		_reset = reset;
+	//	}
+
+	//	public StreamConfiguration UpdateAppResources ()
+	//	{
+	//		if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources ();
+	//		return this;
+	//	}
+
+	//	public StreamConfiguration UpdateAppDirectory ()
+	//	{
+	//		if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory ();
+	//		return this;
+	//	}
+
+	//	// Additional update methods for each location here
+
+	//	private void LoadAppResources ()
+	//	{
+	//		// Load AppResources logic here
+	//	}
+
+	//	private void LoadAppDirectory ()
+	//	{
+	//		// Load AppDirectory logic here
+	//	}
+	//}
 }

+ 1 - 1
Terminal.Gui/Configuration/ConfigurationManagerEventArgs.cs

@@ -17,7 +17,7 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// Event arguments for the <see cref="ConfigurationManager.ThemeManager"/> events.
+	/// Event arguments for the <see cref="ThemeManager"/> events.
 	/// </summary>
 	public class ThemeManagerEventArgs : EventArgs {
 		/// <summary>

+ 4 - 1
Terminal.Gui/Configuration/DictionaryJsonConverter.cs

@@ -4,10 +4,13 @@ using System.Text.Json.Serialization;
 using System.Text.Json;
 
 namespace Terminal.Gui {
-
 	class DictionaryJsonConverter<T> : JsonConverter<Dictionary<string, T>> {
 		public override Dictionary<string, T> Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 		{
+			if (reader.TokenType != JsonTokenType.StartArray) {
+				throw new JsonException ($"Expected a JSON array (\"[ {{ ... }} ]\"), but got \"{reader.TokenType}\".");
+			}
+
 			var dictionary = new Dictionary<string, T> ();
 			while (reader.Read ()) {
 				if (reader.TokenType == JsonTokenType.StartObject) {

+ 1 - 6
Terminal.Gui/Configuration/KeyJsonConverter.cs

@@ -4,11 +4,7 @@ using System.Text.Json;
 using System.Text.Json.Serialization;
 
 namespace Terminal.Gui {
-	/// <summary>
-	/// Json converter for the <see cref="Key"/> class.
-	/// </summary>
-	public class KeyJsonConverter : JsonConverter<Key> {
-		/// <inheritdoc/>
+	class KeyJsonConverter : JsonConverter<Key> {
 		public override Key Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
 		{
 			if (reader.TokenType == JsonTokenType.StartObject) {
@@ -91,7 +87,6 @@ namespace Terminal.Gui {
 			throw new JsonException ($"Unexpected StartObject token when parsing Key: {reader.TokenType}.");
 		}
 
-		/// <inheritdoc/>
 		public override void Write (Utf8JsonWriter writer, Key value, JsonSerializerOptions options)
 		{
 			writer.WriteStartObject ();

+ 119 - 0
Terminal.Gui/Configuration/RuneJsonConverter.cs

@@ -0,0 +1,119 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui;
+/// <summary>
+/// Json converter for <see cref="Rune"/>. Supports
+/// Json converter for <see cref="Rune"/>. Supports
+/// A string as one of:
+/// - unicode char (e.g. "☑")
+/// - U+hex format (e.g. "U+2611")
+/// - \u format (e.g. "\\u2611")
+/// A number
+/// - The unicode code in decimal
+/// </summary>
+internal class RuneJsonConverter : JsonConverter<Rune> {
+	public override Rune Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+	{
+		switch (reader.TokenType) {
+		case JsonTokenType.String: {
+				var value = reader.GetString ();
+				int first = RuneExtensions.MaxUnicodeCodePoint + 1;
+				int second = RuneExtensions.MaxUnicodeCodePoint + 1;
+
+				if (value.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || value.StartsWith ("\\U", StringComparison.OrdinalIgnoreCase)) {
+					// Handle encoded single char, surrogate pair, or combining mark + char
+					var codePoints = Regex.Matches (value, @"(?:\\[uU]\+?|U\+)([0-9A-Fa-f]{1,8})")
+						.Cast<Match> ()
+						.Select (match => uint.Parse (match.Groups [1].Value, NumberStyles.HexNumber))
+						.ToArray ();
+
+					if (codePoints.Length == 0 || codePoints.Length > 2) {
+						throw new JsonException ($"Invalid Rune: {value}.");
+					}
+
+					if (codePoints.Length > 0) {
+						first = (int)codePoints [0];
+					}
+
+					if (codePoints.Length == 2) {
+						second = (int)codePoints [1];
+					}
+				} else {
+					// Handle single character, surrogate pair, or combining mark + char
+					if (value.Length == 0 || value.Length > 2) {
+						throw new JsonException ($"Invalid Rune: {value}.");
+					}
+
+					if (value.Length > 0) {
+						first = value [0];
+					}
+					if (value.Length == 2) {
+						second = value [1];
+					}
+				}
+
+				Rune result;
+				if (second == RuneExtensions.MaxUnicodeCodePoint + 1) {
+					// Single codepoint
+					if (!Rune.TryCreate (first, out result)) {
+						throw new JsonException ($"Invalid Rune: {value}.");
+					}
+					return result;
+				}
+
+				// Surrogate pair?
+				if (Rune.TryCreate ((char)first, (char)second, out result)) {
+					return result;
+				}
+
+				if (!Rune.IsValid (second)) {
+					throw new JsonException ($"The second codepoint is not valid: {second} in ({value})");
+				}
+
+				var cm = new Rune (second);
+				if (!cm.IsCombiningMark ()) {
+					throw new JsonException ($"The second codepoint is not a combining mark: {cm} in ({value})");
+				}
+
+				// not a surrogate pair, so a combining mark + char?
+				var combined = string.Concat ((char)first, (char)second).Normalize ();
+
+				if (!Rune.IsValid (combined [0])) {
+					throw new JsonException ($"Invalid combined Rune ({value})");
+				}
+
+				return new Rune (combined [0]);
+			}
+		case JsonTokenType.Number: {
+				uint num = reader.GetUInt32 ();
+				if (Rune.IsValid (num)) {
+					return new Rune (num);
+				}
+				throw new JsonException ($"Invalid Rune (not a scalar Unicode value): {num}.");
+			}
+		default:
+			throw new JsonException ($"Unexpected token when parsing Rune: {reader.TokenType}.");
+		}
+	}
+
+	public override void Write (Utf8JsonWriter writer, Rune value, JsonSerializerOptions options)
+	{
+		// HACK: Writes a JSON comment in addition to the glyph to ease debugging.
+		// Technically, JSON comments are not valid, but we use relaxed decoding
+		// (ReadCommentHandling = JsonCommentHandling.Skip)
+		writer.WriteCommentValue ($"(U+{value.Value:X8})");
+		var printable = value.MakePrintable ();
+		if (printable == Rune.ReplacementChar) {
+			writer.WriteStringValue (value.ToString ());
+		} else {
+			writer.WriteRawValue ($"\"{value}\"");
+		}
+	}
+}
+#pragma warning restore 1591

+ 47 - 184
Terminal.Gui/Configuration/Scope.cs

@@ -3,211 +3,74 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
-using System.Text.Json;
-using System.Text.Json.Serialization;
 using static Terminal.Gui.ConfigurationManager;
 
 #nullable enable
 
 namespace Terminal.Gui {
-	public static partial class ConfigurationManager {
 
+	/// <summary>
+	/// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
+	/// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
+	/// </summary>
+	public class Scope<T> : Dictionary<string, ConfigProperty> { //, IScope<Scope<T>> {
 		/// <summary>
-		/// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
-		/// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
+		/// Crates a new instance.
 		/// </summary>
-		public class Scope<T> : Dictionary<string, ConfigProperty> { //, IScope<Scope<T>> {
-			/// <summary>
-			/// Crates a new instance.
-			/// </summary>
-			public Scope () : base (StringComparer.InvariantCultureIgnoreCase)
-			{
-				foreach (var p in GetScopeProperties ()) {
-					Add (p.Key, new ConfigProperty () { PropertyInfo = p.Value.PropertyInfo, PropertyValue = null });
-				}
-			}
-
-			private IEnumerable<KeyValuePair<string, ConfigProperty>> GetScopeProperties ()
-			{
-				return ConfigurationManager._allConfigProperties!.Where (cp =>
-					(cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty))
-					as SerializableConfigurationProperty)?.Scope == GetType ());
-			}
-
-			/// <summary>
-			/// Updates this instance from the specified source scope.
-			/// </summary>
-			/// <param name="source"></param>
-			/// <returns>The updated scope (this).</returns>
-			public Scope<T>? Update (Scope<T> source)
-			{
-				foreach (var prop in source) {
-					if (ContainsKey (prop.Key))
-						this [prop.Key].PropertyValue = this [prop.Key].UpdateValueFrom (prop.Value.PropertyValue!);
-					else {
-						this [prop.Key].PropertyValue = prop.Value.PropertyValue;
-					}
-				}
-				return this;
+		public Scope () : base (StringComparer.InvariantCultureIgnoreCase)
+		{
+			foreach (var p in GetScopeProperties ()) {
+				Add (p.Key, new ConfigProperty () { PropertyInfo = p.Value.PropertyInfo, PropertyValue = null });
 			}
+		}
 
-			/// <summary>
-			/// Retrieves the values of the properties of this scope from their corresponding static properties.
-			/// </summary>
-			public void RetrieveValues ()
-			{
-				foreach (var p in this.Where (cp => cp.Value.PropertyInfo != null)) {
-					p.Value.RetrieveValue ();
-				}
-			}
+		private IEnumerable<KeyValuePair<string, ConfigProperty>> GetScopeProperties ()
+		{
+			return ConfigurationManager._allConfigProperties!.Where (cp =>
+				(cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty))
+				as SerializableConfigurationProperty)?.Scope == GetType ());
+		}
 
-			/// <summary>
-			/// Applies the values of the properties of this scope to their corresponding static properties.
-			/// </summary>
-			/// <returns></returns>
-			internal virtual bool Apply ()
-			{
-				bool set = false;
-				foreach (var p in this.Where (t => t.Value != null && t.Value.PropertyValue != null)) {
-					if (p.Value.Apply ()) {
-						set = true;
-					}
+		/// <summary>
+		/// Updates this instance from the specified source scope.
+		/// </summary>
+		/// <param name="source"></param>
+		/// <returns>The updated scope (this).</returns>
+		public Scope<T>? Update (Scope<T> source)
+		{
+			foreach (var prop in source) {
+				if (ContainsKey (prop.Key))
+					this [prop.Key].PropertyValue = this [prop.Key].UpdateValueFrom (prop.Value.PropertyValue!);
+				else {
+					this [prop.Key].PropertyValue = prop.Value.PropertyValue;
 				}
-				return set;
 			}
+			return this;
 		}
 
 		/// <summary>
-		/// Converts <see cref="Scope{T}"/> instances to/from JSON. Does all the heavy lifting of reading/writing
-		/// config data to/from <see cref="ConfigurationManager"/> JSON documents.
+		/// Retrieves the values of the properties of this scope from their corresponding static properties.
 		/// </summary>
-		/// <typeparam name="scopeT"></typeparam>
-		public class ScopeJsonConverter<scopeT> : JsonConverter<scopeT> where scopeT : Scope<scopeT> {
-			// See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
-			internal abstract class ReadHelper {
-				public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
-			}
-
-			internal class ReadHelper<converterT> : ReadHelper {
-				private readonly ReadDelegate _readDelegate;
-				private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
-				public ReadHelper (object converter)
-					=> _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read");
-				public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
-					=> _readDelegate.Invoke (ref reader, type, options);
-			}
-
-			/// <inheritdoc/>
-			public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
-			{
-				if (reader.TokenType != JsonTokenType.StartObject) {
-					throw new JsonException ($"Expected a JSON object, but got \"{reader.TokenType}\".");
-				}
-
-				var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!;
-				while (reader.Read ()) {
-					if (reader.TokenType == JsonTokenType.EndObject) {
-						return scope!;
-					}
-					if (reader.TokenType != JsonTokenType.PropertyName) {
-						throw new JsonException ($"Expected a JSON property name, but got \"{reader.TokenType}\".");
-					}
-					var propertyName = reader.GetString ();
-					reader.Read ();
-
-					if (propertyName != null && scope!.TryGetValue (propertyName, out var configProp)) {
-						// This property name was found in the Scope's ScopeProperties dictionary
-						// Figure out if it needs a JsonConverter and if so, create one
-						var propertyType = configProp?.PropertyInfo?.PropertyType!;
-						if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
-							var converter = Activator.CreateInstance (jca.ConverterType!)!;
-							if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
-								var factory = (JsonConverterFactory)converter;
-								if (propertyType != null && factory.CanConvert (propertyType)) {
-									converter = factory.CreateConverter (propertyType, options);
-								}
-							}
-							var readHelper = Activator.CreateInstance ((Type?)typeof (ReadHelper<>).MakeGenericType (typeof (scopeT), propertyType!)!, converter) as ReadHelper;
-							try {
-								scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options);
-							} catch (NotSupportedException e) {
-								throw new JsonException ($"Error reading property \"{propertyName}\" of type \"{propertyType?.Name}\".", e);
-							}
-						} else {
-							try {
-								scope! [propertyName].PropertyValue = JsonSerializer.Deserialize (ref reader, propertyType!, options);
-							} catch (Exception ex) {
-								System.Diagnostics.Debug.WriteLine ($"scopeT Read: {ex}");
-							}
-						}
-					} else {
-						// It is not a config property. Maybe it's just a property on the Scope with [JsonInclude]
-						// like ScopeSettings.$schema...
-						var property = scope!.GetType ().GetProperties ().Where (p => {
-							var jia = p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as JsonIncludeAttribute;
-							if (jia != null) {
-								var jpna = p.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
-								if (jpna?.Name == propertyName) {
-									// Bit of a hack, modifying propertyName in an enumerator...
-									propertyName = p.Name;
-									return true;
-								}
-
-								return p.Name == propertyName;
-							}
-							return false;
-						}).FirstOrDefault ();
-
-						if (property != null) {
-							var prop = scope.GetType ().GetProperty (propertyName!)!;
-							prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, options));
-						} else {
-							// Unknown property
-							throw new JsonException ($"Unknown property name \"{propertyName}\".");
-						}
-					}
-				}
-				throw new JsonException ();
+		public void RetrieveValues ()
+		{
+			foreach (var p in this.Where (cp => cp.Value.PropertyInfo != null)) {
+				p.Value.RetrieveValue ();
 			}
+		}
 
-			/// <inheritdoc/>
-			public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
-			{
-				writer.WriteStartObject ();
-
-				var properties = scope!.GetType ().GetProperties ().Where (p => p.GetCustomAttribute (typeof (JsonIncludeAttribute)) != null);
-				foreach (var p in properties) {
-					writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
-					JsonSerializer.Serialize (writer, scope.GetType ().GetProperty (p.Name)?.GetValue (scope), options);
-				}
-
-				foreach (var p in from p in scope
-						  .Where (cp =>
-							cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is
-							SerializableConfigurationProperty scp && scp?.Scope == typeof (scopeT))
-						  where p.Value.PropertyValue != null
-						  select p) {
-
-					writer.WritePropertyName (p.Key);
-					var propertyType = p.Value.PropertyInfo?.PropertyType;
-
-					if (propertyType != null && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
-						var converter = Activator.CreateInstance (jca.ConverterType!)!;
-						if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
-							var factory = (JsonConverterFactory)converter;
-							if (factory.CanConvert (propertyType)) {
-								converter = factory.CreateConverter (propertyType, options)!;
-							}
-						}
-						if (p.Value.PropertyValue != null) {
-							converter.GetType ().GetMethod ("Write")?.Invoke (converter, new object [] { writer, p.Value.PropertyValue, options });
-						}
-					} else {
-						JsonSerializer.Serialize (writer, p.Value.PropertyValue, options);
-					}
+		/// <summary>
+		/// Applies the values of the properties of this scope to their corresponding static properties.
+		/// </summary>
+		/// <returns></returns>
+		internal virtual bool Apply ()
+		{
+			bool set = false;
+			foreach (var p in this.Where (t => t.Value != null && t.Value.PropertyValue != null)) {
+				if (p.Value.Apply ()) {
+					set = true;
 				}
-				writer.WriteEndObject ();
 			}
+			return set;
 		}
 	}
 }

+ 140 - 0
Terminal.Gui/Configuration/ScopeJsonConverter.cs

@@ -0,0 +1,140 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Converts <see cref="Scope{T}"/> instances to/from JSON. Does all the heavy lifting of reading/writing
+/// config data to/from <see cref="ConfigurationManager"/> JSON documents.
+/// </summary>
+/// <typeparam name="scopeT"></typeparam>
+internal class ScopeJsonConverter<scopeT> : JsonConverter<scopeT> where scopeT : Scope<scopeT> {
+	// See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
+	internal abstract class ReadHelper {
+		public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
+	}
+
+	internal class ReadHelper<converterT> : ReadHelper {
+		private readonly ReadDelegate _readDelegate;
+		private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
+		public ReadHelper (object converter)
+			=> _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read");
+		public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
+			=> _readDelegate.Invoke (ref reader, type, options);
+	}
+
+	public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+	{
+		if (reader.TokenType != JsonTokenType.StartObject) {
+			throw new JsonException ($"Expected a JSON object (\"{{ \"propName\" : ... }}\"), but got \"{reader.TokenType}\".");
+		}
+
+		var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!;
+		while (reader.Read ()) {
+			if (reader.TokenType == JsonTokenType.EndObject) {
+				return scope!;
+			}
+			if (reader.TokenType != JsonTokenType.PropertyName) {
+				throw new JsonException ($"Expected a JSON property name, but got \"{reader.TokenType}\".");
+			}
+			var propertyName = reader.GetString ();
+			reader.Read ();
+
+			if (propertyName != null && scope!.TryGetValue (propertyName, out var configProp)) {
+				// This property name was found in the Scope's ScopeProperties dictionary
+				// Figure out if it needs a JsonConverter and if so, create one
+				var propertyType = configProp?.PropertyInfo?.PropertyType!;
+				if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
+					var converter = Activator.CreateInstance (jca.ConverterType!)!;
+					if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
+						var factory = (JsonConverterFactory)converter;
+						if (propertyType != null && factory.CanConvert (propertyType)) {
+							converter = factory.CreateConverter (propertyType, options);
+						}
+					}
+					var readHelper = Activator.CreateInstance ((Type?)typeof (ReadHelper<>).MakeGenericType (typeof (scopeT), propertyType!)!, converter) as ReadHelper;
+					try {
+						scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options);
+					} catch (NotSupportedException e) {
+						throw new JsonException ($"Error reading property \"{propertyName}\" of type \"{propertyType?.Name}\".", e);
+					}
+				} else {
+					try {
+						scope! [propertyName].PropertyValue = JsonSerializer.Deserialize (ref reader, propertyType!, options);
+					} catch (Exception ex) {
+						System.Diagnostics.Debug.WriteLine ($"scopeT Read: {ex}");
+					}
+				}
+			} else {
+				// It is not a config property. Maybe it's just a property on the Scope with [JsonInclude]
+				// like ScopeSettings.$schema...
+				var property = scope!.GetType ().GetProperties ().Where (p => {
+					var jia = p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as JsonIncludeAttribute;
+					if (jia != null) {
+						var jpna = p.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
+						if (jpna?.Name == propertyName) {
+							// Bit of a hack, modifying propertyName in an enumerator...
+							propertyName = p.Name;
+							return true;
+						}
+
+						return p.Name == propertyName;
+					}
+					return false;
+				}).FirstOrDefault ();
+
+				if (property != null) {
+					var prop = scope.GetType ().GetProperty (propertyName!)!;
+					prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, options));
+				} else {
+					// Unknown property
+					throw new JsonException ($"Unknown property name \"{propertyName}\".");
+				}
+			}
+		}
+		throw new JsonException ();
+	}
+
+	public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
+	{
+		writer.WriteStartObject ();
+
+		var properties = scope!.GetType ().GetProperties ().Where (p => p.GetCustomAttribute (typeof (JsonIncludeAttribute)) != null);
+		foreach (var p in properties) {
+			writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
+			JsonSerializer.Serialize (writer, scope.GetType ().GetProperty (p.Name)?.GetValue (scope), options);
+		}
+
+		foreach (var p in from p in scope
+					.Where (cp =>
+					cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is
+					SerializableConfigurationProperty scp && scp?.Scope == typeof (scopeT))
+					where p.Value.PropertyValue != null
+					select p) {
+
+			writer.WritePropertyName (p.Key);
+			var propertyType = p.Value.PropertyInfo?.PropertyType;
+
+			if (propertyType != null && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) {
+				var converter = Activator.CreateInstance (jca.ConverterType!)!;
+				if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) {
+					var factory = (JsonConverterFactory)converter;
+					if (factory.CanConvert (propertyType)) {
+						converter = factory.CreateConverter (propertyType, options)!;
+					}
+				}
+				if (p.Value.PropertyValue != null) {
+					converter.GetType ().GetMethod ("Write")?.Invoke (converter, new object [] { writer, p.Value.PropertyValue, options });
+				}
+			} else {
+				JsonSerializer.Serialize (writer, p.Value.PropertyValue, options);
+			}
+		}
+		writer.WriteEndObject ();
+	}
+}

+ 28 - 0
Terminal.Gui/Configuration/SerializableConfigurationProperty.cs

@@ -0,0 +1,28 @@
+using System;
+
+#nullable enable
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// An attribute that can be applied to a property to indicate that it should included in the configuration file.
+/// </summary>
+/// <example>
+/// 	[SerializableConfigurationProperty(Scope = typeof(Configuration.ThemeManager.ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
+///	public static LineStyle DefaultBorderStyle {
+///	...
+/// </example>
+[AttributeUsage (AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+public class SerializableConfigurationProperty : System.Attribute {
+	/// <summary>
+	/// Specifies the scope of the property. 
+	/// </summary>
+	public Type? Scope { get; set; }
+
+	/// <summary>
+	/// If <see langword="true"/>, the property will be serialized to the configuration file using only the property name
+	/// as the key. If <see langword="false"/>, the property will be serialized to the configuration file using the
+	/// property name pre-pended with the classname (e.g. <c>Application.UseSystemConsole</c>).
+	/// </summary>
+	public bool OmitClassName { get; set; }
+}

+ 97 - 99
Terminal.Gui/Configuration/SettingsScope.cs

@@ -8,113 +8,111 @@ using System.Text.Json.Serialization;
 
 #nullable enable
 
-namespace Terminal.Gui {
-	public static partial class ConfigurationManager {
-		/// <summary>
-		/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties 
-		/// attributed with  <see cref="SettingsScope"/>.
-		/// </summary>
-		/// <example><code>
-		///  {
-		///    "$schema" : "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
-		///    "Application.UseSystemConsole" : true,
-		///    "Theme" : "Default",
-		///    "Themes": {
-		///    },
-		///  },
-		/// </code></example>
-		/// <remarks>
-		/// </remarks>
-		[JsonConverter (typeof (ScopeJsonConverter<SettingsScope>))]
-		public class SettingsScope : Scope<SettingsScope> {
-			/// <summary>
-			/// Points to our JSON schema.
-			/// </summary>
-			[JsonInclude, JsonPropertyName ("$schema")]
-			public string Schema { get; set; } = "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json";
+namespace Terminal.Gui;
 
-			/// <summary>
-			/// The list of paths to the configuration files.
-			/// </summary>
-			public List<string> Sources = new List<string> ();
+/// <summary>
+/// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties 
+/// attributed with  <see cref="SettingsScope"/>.
+/// </summary>
+/// <example><code>
+///  {
+///    "$schema" : "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
+///    "Application.UseSystemConsole" : true,
+///    "Theme" : "Default",
+///    "Themes": {
+///    },
+///  },
+/// </code></example>
+/// <remarks>
+/// </remarks>
+[JsonConverter (typeof (ScopeJsonConverter<SettingsScope>))]
+public class SettingsScope : Scope<SettingsScope> {
+	/// <summary>
+	/// Points to our JSON schema.
+	/// </summary>
+	[JsonInclude, JsonPropertyName ("$schema")]
+	public string Schema { get; set; } = "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json";
 
-			/// <summary>
-			/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
-			/// </summary>
-			/// <param name="stream">Json document to update the settings with.</param>
-			/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
-			public SettingsScope? Update (Stream stream, string source)
-			{
-				// Update the existing settings with the new settings.
-				try {
-					Update (JsonSerializer.Deserialize<SettingsScope> (stream, serializerOptions)!);
-					OnUpdated ();
-					Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\"");
-					Sources.Add (source);
-					return this;
-				} catch (JsonException e) {
-					if (ThrowOnJsonErrors ?? false) {
-						throw;
-					} else {
-						AddJsonError ($"Error deserializing {source}: {e.Message}");
-					}
-				}
-				return this;
-			}
-			
-			/// <summary>
-			/// Updates the <see cref="SettingsScope"/> with the settings in a JSON file.
-			/// </summary>
-			/// <param name="filePath"></param>
-			public SettingsScope? Update (string filePath)
-			{
-				var realPath = filePath.Replace("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
-				if (!File.Exists (realPath)) {
-					Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist.");
-					Sources.Add (filePath);
-					return this;
-				}
+	/// <summary>
+	/// The list of paths to the configuration files.
+	/// </summary>
+	public List<string> Sources = new List<string> ();
 
-				var stream = File.OpenRead (realPath);
-				return Update (stream, filePath);
+	/// <summary>
+	/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
+	/// </summary>
+	/// <param name="stream">Json document to update the settings with.</param>
+	/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+	public SettingsScope? Update (Stream stream, string source)
+	{
+		// Update the existing settings with the new settings.
+		try {
+			Update (JsonSerializer.Deserialize<SettingsScope> (stream, ConfigurationManager._serializerOptions)!);
+			ConfigurationManager.OnUpdated ();
+			Debug.WriteLine ($"ConfigurationManager: Read configuration from \"{source}\"");
+			Sources.Add (source);
+			return this;
+		} catch (JsonException e) {
+			if (ConfigurationManager.ThrowOnJsonErrors ?? false) {
+				throw;
+			} else {
+				ConfigurationManager.AddJsonError ($"Error deserializing {source}: {e.Message}");
 			}
+		}
+		return this;
+	}
 
-			/// <summary>
-			/// Updates the <see cref="SettingsScope"/> with the settings from a Json resource.
-			/// </summary>
-			/// <param name="assembly"></param>
-			/// <param name="resourceName"></param>
-			public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName)
-			{
-				if (resourceName == null || string.IsNullOrEmpty (resourceName)) {
-					Debug.WriteLine ($"ConfigurationManager: Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\".");
-					return this;
-				}
-				
-				using Stream? stream = assembly.GetManifestResourceStream (resourceName)!;
-				if (stream == null) {
-					Debug.WriteLine ($"ConfigurationManager: Failed to read resource \"{resourceName}\" from \"{assembly.GetName ().Name}\".");
-					return this;
-				}
+	/// <summary>
+	/// Updates the <see cref="SettingsScope"/> with the settings in a JSON file.
+	/// </summary>
+	/// <param name="filePath"></param>
+	public SettingsScope? Update (string filePath)
+	{
+		var realPath = filePath.Replace ("~", Environment.GetFolderPath (Environment.SpecialFolder.UserProfile));
+		if (!File.Exists (realPath)) {
+			Debug.WriteLine ($"ConfigurationManager: Configuration file \"{realPath}\" does not exist.");
+			Sources.Add (filePath);
+			return this;
+		}
 
-				return Update (stream, $"resource://[{assembly.GetName().Name}]/{resourceName}");
-			}
+		var stream = File.OpenRead (realPath);
+		return Update (stream, filePath);
+	}
 
-			/// <summary>
-			/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
-			/// </summary>
-			/// <param name="json">Json document to update the settings with.</param>
-			/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
-			public SettingsScope? Update (string json, string source)
-			{
-				var stream = new MemoryStream ();
-				var writer = new StreamWriter (stream);
-				writer.Write (json);
-				writer.Flush ();
-				stream.Position = 0;
+	/// <summary>
+	/// Updates the <see cref="SettingsScope"/> with the settings from a Json resource.
+	/// </summary>
+	/// <param name="assembly"></param>
+	/// <param name="resourceName"></param>
+	public SettingsScope? UpdateFromResource (Assembly assembly, string resourceName)
+	{
+		if (resourceName == null || string.IsNullOrEmpty (resourceName)) {
+			Debug.WriteLine ($"ConfigurationManager: Resource \"{resourceName}\" does not exist in \"{assembly.GetName ().Name}\".");
+			return this;
+		}
 
-				return Update (stream, source);
-			}
+		using Stream? stream = assembly.GetManifestResourceStream (resourceName)!;
+		if (stream == null) {
+			Debug.WriteLine ($"ConfigurationManager: Failed to read resource \"{resourceName}\" from \"{assembly.GetName ().Name}\".");
+			return this;
 		}
+
+		return Update (stream, $"resource://[{assembly.GetName ().Name}]/{resourceName}");
+	}
+
+	/// <summary>
+	/// Updates the <see cref="SettingsScope"/> with the settings in a JSON string.
+	/// </summary>
+	/// <param name="json">Json document to update the settings with.</param>
+	/// <param name="source">The source (filename/resource name) the Json document was read from.</param>
+	public SettingsScope? Update (string json, string source)
+	{
+		var stream = new MemoryStream ();
+		var writer = new StreamWriter (stream);
+		writer.Write (json);
+		writer.Flush ();
+		stream.Position = 0;
+
+		return Update (stream, source);
 	}
 }

+ 207 - 0
Terminal.Gui/Configuration/ThemeManager.cs

@@ -0,0 +1,207 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Text.Json.Serialization;
+
+#nullable enable
+
+namespace Terminal.Gui; 
+
+/// <summary>
+/// Contains a dictionary of the <see cref="ThemeManager.Theme"/>s for a Terminal.Gui application.
+/// </summary>
+/// <remarks>
+/// <para>
+/// A Theme is a collection of settings that are named. The default theme is named "Default".
+/// </para>
+/// <para>
+/// The <see cref="ThemeManager.Theme"/> property is used to detemrine the currently active theme. 
+/// </para>
+/// </remarks>
+/// <para>
+/// <see cref="ThemeManager"/> is a singleton class. It is created when the first <see cref="ThemeManager"/> property is accessed.
+/// Accessing <see cref="ThemeManager.Instance"/> is the same as accessing <see cref="ConfigurationManager.Themes"/>.
+/// </para>
+/// <example><code>
+/// 	"Themes": [
+/// 	{
+/// 		"Default": {
+/// 			"ColorSchemes": [
+/// 			{
+/// 			"TopLevel": {
+/// 			"Normal": {
+/// 				"Foreground": "BrightGreen",
+/// 				"Background": "Black"
+/// 			},
+/// 			"Focus": {
+/// 			"Foreground": "White",
+/// 				"Background": "Cyan"
+/// 
+/// 			},
+/// 			"HotNormal": {
+/// 				"Foreground": "Brown",
+/// 				"Background": "Black"
+/// 
+/// 			},
+/// 			"HotFocus": {
+/// 				"Foreground": "Blue",
+/// 				"Background": "Cyan"
+/// 			},
+/// 			"Disabled": {
+/// 				"Foreground": "DarkGray",
+/// 				"Background": "Black"
+/// 
+/// 			}
+/// 		}
+/// 	}
+/// </code></example> 
+public class ThemeManager : IDictionary<string, ThemeScope> {
+	private static readonly ThemeManager _instance = new ThemeManager ();
+	static ThemeManager () { } // Make sure it's truly lazy
+	private ThemeManager () { } // Prevent instantiation outside
+
+	/// <summary>
+	/// Class is a singleton...
+	/// </summary>
+	public static ThemeManager Instance { get { return _instance; } }
+
+	private static string _theme = string.Empty;
+
+	/// <summary>
+	/// The currently selected theme. This is the internal version; see <see cref="Theme"/>.
+	/// </summary>
+	[JsonInclude, SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("Theme")]
+	internal static string SelectedTheme {
+		get => _theme;
+		set {
+			var oldTheme = _theme;
+			_theme = value;
+			if (oldTheme != _theme &&
+				ConfigurationManager.Settings! ["Themes"]?.PropertyValue is Dictionary<string, ThemeScope> themes &&
+				themes.ContainsKey (_theme)) {
+				ConfigurationManager.Settings! ["Theme"].PropertyValue = _theme;
+				Instance.OnThemeChanged (oldTheme);
+			}
+		}
+	}
+
+	/// <summary>
+	/// Gets or sets the currently selected theme. The value is persisted to the "Theme"
+	/// property.
+	/// </summary>
+	[JsonIgnore]
+	public string Theme {
+		get => ThemeManager.SelectedTheme;
+		set {
+			ThemeManager.SelectedTheme = value;
+		}
+	}
+
+	/// <summary>
+	/// Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.
+	/// </summary>
+	internal void OnThemeChanged (string theme)
+	{
+		Debug.WriteLine ($"Themes.OnThemeChanged({theme}) -> {Theme}");
+		ThemeChanged?.Invoke (this, new ThemeManagerEventArgs (theme));
+	}
+
+	/// <summary>
+	/// Event fired he selected theme has changed.
+	/// application.
+	/// </summary>
+	public event EventHandler<ThemeManagerEventArgs>? ThemeChanged;
+
+	/// <summary>
+	/// Holds the <see cref="ThemeScope"/> definitions. 
+	/// </summary>
+	[JsonInclude, JsonConverter (typeof (DictionaryJsonConverter<ThemeScope>))]
+	[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
+	public static Dictionary<string, ThemeScope>? Themes {
+		get => ConfigurationManager.Settings? ["Themes"]?.PropertyValue as Dictionary<string, ThemeScope>; // themes ?? new Dictionary<string, ThemeScope> ();
+		set {
+			//if (themes == null || value == null) {
+			//	themes = value;
+			//} else {
+			//	themes = (Dictionary<string, ThemeScope>)DeepMemberwiseCopy (value!, themes!)!;
+			//}
+			ConfigurationManager.Settings! ["Themes"].PropertyValue = value;
+		}
+	}
+
+	internal static void Reset ()
+	{
+		Debug.WriteLine ($"Themes.Reset()");
+
+		Themes?.Clear ();
+		SelectedTheme = string.Empty;
+	}
+
+	internal static void GetHardCodedDefaults ()
+	{
+		Debug.WriteLine ($"Themes.GetHardCodedDefaults()");
+		var theme = new ThemeScope ();
+		theme.RetrieveValues ();
+
+		Themes = new Dictionary<string, ThemeScope> (StringComparer.InvariantCultureIgnoreCase) { { "Default", theme } };
+		SelectedTheme = "Default";
+	}
+
+	#region IDictionary
+#pragma warning disable 1591
+
+	public ICollection<string> Keys => ((IDictionary<string, ThemeScope>)Themes!).Keys;
+	public ICollection<ThemeScope> Values => ((IDictionary<string, ThemeScope>)Themes!).Values;
+	public int Count => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Count;
+	public bool IsReadOnly => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).IsReadOnly;
+	public ThemeScope this [string key] { get => ((IDictionary<string, ThemeScope>)Themes!) [key]; set => ((IDictionary<string, ThemeScope>)Themes!) [key] = value; }
+	public void Add (string key, ThemeScope value)
+	{
+		((IDictionary<string, ThemeScope>)Themes!).Add (key, value);
+	}
+	public bool ContainsKey (string key)
+	{
+		return ((IDictionary<string, ThemeScope>)Themes!).ContainsKey (key);
+	}
+	public bool Remove (string key)
+	{
+		return ((IDictionary<string, ThemeScope>)Themes!).Remove (key);
+	}
+	public bool TryGetValue (string key, out ThemeScope value)
+	{
+		return ((IDictionary<string, ThemeScope>)Themes!).TryGetValue (key, out value!);
+	}
+	public void Add (KeyValuePair<string, ThemeScope> item)
+	{
+		((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Add (item);
+	}
+	public void Clear ()
+	{
+		((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Clear ();
+	}
+	public bool Contains (KeyValuePair<string, ThemeScope> item)
+	{
+		return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Contains (item);
+	}
+	public void CopyTo (KeyValuePair<string, ThemeScope> [] array, int arrayIndex)
+	{
+		((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).CopyTo (array, arrayIndex);
+	}
+	public bool Remove (KeyValuePair<string, ThemeScope> item)
+	{
+		return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Remove (item);
+	}
+	public IEnumerator<KeyValuePair<string, ThemeScope>> GetEnumerator ()
+	{
+		return ((IEnumerable<KeyValuePair<string, ThemeScope>>)Themes!).GetEnumerator ();
+	}
+
+	IEnumerator IEnumerable.GetEnumerator ()
+	{
+		return ((IEnumerable)Themes!).GetEnumerator ();
+	}
+#pragma warning restore 1591
+
+	#endregion
+}

+ 50 - 269
Terminal.Gui/Configuration/ThemeScope.cs

@@ -1,274 +1,55 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Linq;
-using System.Reflection;
-using System.Text.Json.Serialization;
-using static Terminal.Gui.ConfigurationManager;
+using System.Text.Json.Serialization;
 
 #nullable enable
 
-namespace Terminal.Gui {
-
-	public static partial class ConfigurationManager {
-		/// <summary>
-		/// The root object for a Theme. A Theme is a set of settings that are applied to the running <see cref="Application"/>
-		/// as a group.
-		/// </summary>
-		/// <remarks>
-		/// <para>
-		/// </para>
-		/// </remarks>
-		/// <example><code>
-		/// 	"Default": {
-		/// 		"ColorSchemes": [
-		/// 		{
-		/// 		"TopLevel": {
-		/// 		"Normal": {
-		/// 			"Foreground": "BrightGreen",
-		/// 			"Background": "Black"
-		/// 		},
-		/// 		"Focus": {
-		/// 		"Foreground": "White",
-		/// 			"Background": "Cyan"
-		/// 
-		/// 		},
-		/// 		"HotNormal": {
-		/// 			"Foreground": "Brown",
-		/// 			"Background": "Black"
-		/// 
-		/// 		},
-		/// 		"HotFocus": {
-		/// 			"Foreground": "Blue",
-		/// 			"Background": "Cyan"
-		/// 		},
-		/// 		"Disabled": {
-		/// 			"Foreground": "DarkGray",
-		/// 			"Background": "Black"
-		/// 
-		/// 		}
-		/// 	}
-		/// </code></example> 
-		[JsonConverter (typeof (ScopeJsonConverter<ThemeScope>))]
-		public class ThemeScope : Scope<ThemeScope> {
-
-			/// <inheritdoc/>
-			internal override bool Apply ()
-			{
-				var ret = base.Apply ();
-				Application.Driver?.InitalizeColorSchemes ();
-				return ret;
-			}
-		}
-
-		/// <summary>
-		/// Contains a dictionary of the <see cref="ThemeManager.Theme"/>s for a Terminal.Gui application.
-		/// </summary>
-		/// <remarks>
-		/// <para>
-		/// A Theme is a collection of settings that are named. The default theme is named "Default".
-		/// </para>
-		/// <para>
-		/// The <see cref="ThemeManager.Theme"/> property is used to detemrine the currently active theme. 
-		/// </para>
-		/// </remarks>
-		/// <para>
-		/// <see cref="ThemeManager"/> is a singleton class. It is created when the first <see cref="ThemeManager"/> property is accessed.
-		/// Accessing <see cref="ThemeManager.Instance"/> is the same as accessing <see cref="ConfigurationManager.Themes"/>.
-		/// </para>
-		/// <example><code>
-		/// 	"Themes": [
-		/// 	{
-		/// 		"Default": {
-		/// 			"ColorSchemes": [
-		/// 			{
-		/// 			"TopLevel": {
-		/// 			"Normal": {
-		/// 				"Foreground": "BrightGreen",
-		/// 				"Background": "Black"
-		/// 			},
-		/// 			"Focus": {
-		/// 			"Foreground": "White",
-		/// 				"Background": "Cyan"
-		/// 
-		/// 			},
-		/// 			"HotNormal": {
-		/// 				"Foreground": "Brown",
-		/// 				"Background": "Black"
-		/// 
-		/// 			},
-		/// 			"HotFocus": {
-		/// 				"Foreground": "Blue",
-		/// 				"Background": "Cyan"
-		/// 			},
-		/// 			"Disabled": {
-		/// 				"Foreground": "DarkGray",
-		/// 				"Background": "Black"
-		/// 
-		/// 			}
-		/// 		}
-		/// 	}
-		/// </code></example> 
-		public partial class ThemeManager : IDictionary<string, ThemeScope> {
-			private static readonly ThemeManager _instance = new ThemeManager ();
-			static ThemeManager () { } // Make sure it's truly lazy
-			private ThemeManager () { } // Prevent instantiation outside
-
-			/// <summary>
-			/// Class is a singleton...
-			/// </summary>
-			public static ThemeManager Instance { get { return _instance; } }
-
-			private static string _theme = string.Empty;
-
-			/// <summary>
-			/// The currently selected theme. This is the internal version; see <see cref="Theme"/>.
-			/// </summary>
-			[JsonInclude, SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("Theme")]
-			internal static string SelectedTheme {
-				get => _theme;
-				set {
-					var oldTheme = _theme;
-					_theme = value;
-					if (oldTheme != _theme &&
-						ConfigurationManager.Settings! ["Themes"]?.PropertyValue is Dictionary<string, ThemeScope> themes &&
-						themes.ContainsKey (_theme)) {
-						ConfigurationManager.Settings! ["Theme"].PropertyValue = _theme;
-						Instance.OnThemeChanged (oldTheme);
-					}
-				}
-			}
-
-			/// <summary>
-			/// Gets or sets the currently selected theme. The value is persisted to the "Theme"
-			/// property.
-			/// </summary>
-			[JsonIgnore]
-			public string Theme {
-				get => ThemeManager.SelectedTheme;
-				set {
-					ThemeManager.SelectedTheme = value;
-				}
-			}
-
-			/// <summary>
-			/// Called when the selected theme has changed. Fires the <see cref="ThemeChanged"/> event.
-			/// </summary>
-			internal void OnThemeChanged (string theme)
-			{
-				Debug.WriteLine ($"Themes.OnThemeChanged({theme}) -> {Theme}");
-				ThemeChanged?.Invoke (this, new ThemeManagerEventArgs (theme));
-			}
-
-			/// <summary>
-			/// Event fired he selected theme has changed.
-			/// application.
-			/// </summary>
-			public event EventHandler<ThemeManagerEventArgs>? ThemeChanged;
-
-			/// <summary>
-			/// Holds the <see cref="ThemeScope"/> definitions. 
-			/// </summary>
-			[JsonInclude, JsonConverter (typeof (DictionaryJsonConverter<ThemeScope>))]
-			[SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)]
-			public static Dictionary<string, ThemeScope>? Themes {
-				get => Settings? ["Themes"]?.PropertyValue as Dictionary<string, ThemeScope>; // themes ?? new Dictionary<string, ThemeScope> ();
-				set {
-					//if (themes == null || value == null) {
-					//	themes = value;
-					//} else {
-					//	themes = (Dictionary<string, ThemeScope>)DeepMemberwiseCopy (value!, themes!)!;
-					//}
-					Settings! ["Themes"].PropertyValue = value;
-				}
-			}
-
-			internal static void Reset ()
-			{
-				Debug.WriteLine ($"Themes.Reset()");
-
-				Themes?.Clear ();
-				SelectedTheme = string.Empty;
-			}
-
-			internal static void GetHardCodedDefaults ()
-			{
-				Debug.WriteLine ($"Themes.GetHardCodedDefaults()");
-				var theme = new ThemeScope ();
-				theme.RetrieveValues ();
-
-				Themes = new Dictionary<string, ThemeScope> (StringComparer.InvariantCultureIgnoreCase) { { "Default", theme } };
-				SelectedTheme = "Default";
-			}
-
-			#region IDictionary
-			/// <inheritdoc/>
-			public ICollection<string> Keys => ((IDictionary<string, ThemeScope>)Themes!).Keys;
-			/// <inheritdoc/>
-			public ICollection<ThemeScope> Values => ((IDictionary<string, ThemeScope>)Themes!).Values;
-			/// <inheritdoc/>
-			public int Count => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Count;
-			/// <inheritdoc/>
-			public bool IsReadOnly => ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).IsReadOnly;
-			/// <inheritdoc/>
-			public ThemeScope this [string key] { get => ((IDictionary<string, ThemeScope>)Themes!) [key]; set => ((IDictionary<string, ThemeScope>)Themes!) [key] = value; }
-			/// <inheritdoc/>
-			public void Add (string key, ThemeScope value)
-			{
-				((IDictionary<string, ThemeScope>)Themes!).Add (key, value);
-			}
-			/// <inheritdoc/>
-			public bool ContainsKey (string key)
-			{
-				return ((IDictionary<string, ThemeScope>)Themes!).ContainsKey (key);
-			}
-			/// <inheritdoc/>
-			public bool Remove (string key)
-			{
-				return ((IDictionary<string, ThemeScope>)Themes!).Remove (key);
-			}
-			/// <inheritdoc/>
-			public bool TryGetValue (string key, out ThemeScope value)
-			{
-				return ((IDictionary<string, ThemeScope>)Themes!).TryGetValue (key, out value!);
-			}
-			/// <inheritdoc/>
-			public void Add (KeyValuePair<string, ThemeScope> item)
-			{
-				((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Add (item);
-			}
-			/// <inheritdoc/>
-			public void Clear ()
-			{
-				((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Clear ();
-			}
-			/// <inheritdoc/>
-			public bool Contains (KeyValuePair<string, ThemeScope> item)
-			{
-				return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Contains (item);
-			}
-			/// <inheritdoc/>
-			public void CopyTo (KeyValuePair<string, ThemeScope> [] array, int arrayIndex)
-			{
-				((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).CopyTo (array, arrayIndex);
-			}
-			/// <inheritdoc/>
-			public bool Remove (KeyValuePair<string, ThemeScope> item)
-			{
-				return ((ICollection<KeyValuePair<string, ThemeScope>>)Themes!).Remove (item);
-			}
-			/// <inheritdoc/>
-			public IEnumerator<KeyValuePair<string, ThemeScope>> GetEnumerator ()
-			{
-				return ((IEnumerable<KeyValuePair<string, ThemeScope>>)Themes!).GetEnumerator ();
-			}
-
-			IEnumerator IEnumerable.GetEnumerator ()
-			{
-				return ((IEnumerable)Themes!).GetEnumerator ();
-			}
-			#endregion
-		}
+namespace Terminal.Gui; 
+
+/// <summary>
+/// The root object for a Theme. A Theme is a set of settings that are applied to the running <see cref="Application"/>
+/// as a group.
+/// </summary>
+/// <remarks>
+/// <para>
+/// </para>
+/// </remarks>
+/// <example><code>
+/// 	"Default": {
+/// 		"ColorSchemes": [
+/// 		{
+/// 		"TopLevel": {
+/// 		"Normal": {
+/// 			"Foreground": "BrightGreen",
+/// 			"Background": "Black"
+/// 		},
+/// 		"Focus": {
+/// 		"Foreground": "White",
+/// 			"Background": "Cyan"
+/// 
+/// 		},
+/// 		"HotNormal": {
+/// 			"Foreground": "Brown",
+/// 			"Background": "Black"
+/// 
+/// 		},
+/// 		"HotFocus": {
+/// 			"Foreground": "Blue",
+/// 			"Background": "Cyan"
+/// 		},
+/// 		"Disabled": {
+/// 			"Foreground": "DarkGray",
+/// 			"Background": "Black"
+/// 
+/// 		}
+/// 	}
+/// </code></example> 
+[JsonConverter (typeof (ScopeJsonConverter<ThemeScope>))]
+public class ThemeScope : Scope<ThemeScope> {
+
+	/// <inheritdoc/>
+	internal override bool Apply ()
+	{
+		var ret = base.Apply ();
+		Application.Driver?.InitalizeColorSchemes ();
+		return ret;
 	}
 }

+ 43 - 360
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -1,7 +1,7 @@
 //
 // ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations.
 //
-using NStack;
+using System.Text;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -344,6 +344,26 @@ namespace Terminal.Gui {
 		/// </summary>
 		internal string schemeBeingSet = "";
 
+		/// <summary>
+		/// Creates a new instance.
+		/// </summary>
+		public ColorScheme () { }
+
+		/// <summary>
+		/// Creates a new instance, initialized with the values from <paramref name="scheme"/>.
+		/// </summary>
+		/// <param name="scheme">The scheme to initlize the new instance with.</param>
+		public ColorScheme (ColorScheme scheme) : base ()
+		{
+			if (scheme != null) {
+				_normal = scheme.Normal;
+				_focus = scheme.Focus;
+				_hotNormal = scheme.HotNormal;
+				_disabled = scheme.Disabled;
+				_hotFocus = scheme.HotFocus;
+			}
+		}
+
 		/// <summary>
 		/// The foreground and background color for text when the view is not focused, hot, or disabled.
 		/// </summary>
@@ -597,8 +617,8 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Provides the defined <see cref="ColorScheme"/>s.
 		/// </summary>
-		[SerializableConfigurationProperty (Scope = typeof(ThemeScope), OmitClassName = true)]
-		[JsonConverter(typeof(DictionaryJsonConverter<ColorScheme>))]
+		[SerializableConfigurationProperty (Scope = typeof (ThemeScope), OmitClassName = true)]
+		[JsonConverter (typeof (DictionaryJsonConverter<ColorScheme>))]
 		public static Dictionary<string, ColorScheme> ColorSchemes { get; private set; }
 	}
 
@@ -659,12 +679,13 @@ namespace Terminal.Gui {
 		/// <remarks>Works under Xterm-like terminal otherwise this is equivalent to <see ref="Block"/></remarks>
 		BoxFix = 0x02020164,
 	}
-	
+
 	/// <summary>
 	/// ConsoleDriver is an abstract class that defines the requirements for a console driver.  
 	/// There are currently three implementations: <see cref="CursesDriver"/> (for Unix and Mac), <see cref="WindowsDriver"/>, and <see cref="NetDriver"/> that uses the .NET Console API.
 	/// </summary>
 	public abstract class ConsoleDriver {
+
 		/// <summary>
 		/// The handler fired when the terminal is resized.
 		/// </summary>
@@ -731,28 +752,25 @@ namespace Terminal.Gui {
 		public abstract void Move (int col, int row);
 
 		/// <summary>
-		/// Adds the specified rune to the display at the current cursor position.
+		/// Tests if the specified rune is supported by the driver.
 		/// </summary>
-		/// <param name="rune">Rune to add.</param>
-		public abstract void AddRune (Rune rune);
-
-		/// <summary>
-		/// Ensures a Rune is not a control character and can be displayed by translating characters below 0x20
-		/// to equivalent, printable, Unicode chars.
-		/// </summary>
-		/// <param name="c">Rune to translate</param>
-		/// <returns></returns>
-		public static Rune MakePrintable (Rune c)
+		/// <param name="rune"></param>
+		/// <returns><see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver
+		/// does not support displaying this rune.</returns>
+		public virtual bool IsRuneSupported (Rune rune)
 		{
-			if (c <= 0x1F || (c >= 0X7F && c <= 0x9F)) {
-				// ASCII (C0) control characters.
-				// C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1)
-				return new Rune (c + 0x2400);
+			if (rune.Value > RuneExtensions.MaxUnicodeCodePoint) {
+				return false;
 			}
-
-			return c;
+			return true;
 		}
-
+		
+		/// <summary>
+		/// Adds the specified rune to the display at the current cursor position.
+		/// </summary>
+		/// <param name="rune">Rune to add.</param>
+		public abstract void AddRune (Rune rune);
+		
 		/// <summary>
 		/// Ensures that the column and line are in a valid range from the size of the driver.
 		/// </summary>
@@ -767,7 +785,7 @@ namespace Terminal.Gui {
 		/// Adds the <paramref name="str"/> to the display at the cursor position.
 		/// </summary>
 		/// <param name="str">String.</param>
-		public abstract void AddStr (ustring str);
+		public abstract void AddStr (string str);
 
 		/// <summary>
 		/// Prepare the driver and set the key and mouse events handlers.
@@ -907,12 +925,12 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="rect"></param>
 		/// <param name="rune"></param>
-		public virtual void FillRect (Rect rect, System.Rune rune = default)
+		public virtual void FillRect (Rect rect, Rune rune = default)
 		{
 			for (var r = rect.Y; r < rect.Y + rect.Height; r++) {
 				for (var c = rect.X; c < rect.X + rect.Width; c++) {
 					Application.Driver.Move (c, r);
-					Application.Driver.AddRune (rune == default ? ' ' : rune);
+					Application.Driver.AddRune ((Rune)(rune == default ? ' ' : rune.Value));
 				}
 			}
 		}
@@ -980,341 +998,6 @@ namespace Terminal.Gui {
 		/// </summary>
 		public abstract void CookMouse ();
 
-		/// <summary>
-		/// Horizontal line character.
-		/// </summary>
-		public Rune HLine = '\u2500';
-
-		/// <summary>
-		/// Vertical line character.
-		/// </summary>
-		public Rune VLine = '\u2502';
-
-		/// <summary>
-		/// Stipple pattern
-		/// </summary>
-		public Rune Stipple = '\u2591';
-
-		/// <summary>
-		/// Diamond character
-		/// </summary>
-		public Rune Diamond = '\u25ca';
-
-		/// <summary>
-		/// Upper left corner
-		/// </summary>
-		public Rune ULCorner = '\u250C';
-
-		/// <summary>
-		/// Lower left corner
-		/// </summary>
-		public Rune LLCorner = '\u2514';
-
-		/// <summary>
-		/// Upper right corner
-		/// </summary>
-		public Rune URCorner = '\u2510';
-
-		/// <summary>
-		/// Lower right corner
-		/// </summary>
-		public Rune LRCorner = '\u2518';
-
-		/// <summary>
-		/// Left tee
-		/// </summary>
-		public Rune LeftTee = '\u251c';
-
-		/// <summary>
-		/// Right tee
-		/// </summary>
-		public Rune RightTee = '\u2524';
-
-		/// <summary>
-		/// Top tee
-		/// </summary>
-		public Rune TopTee = '\u252c';
-
-		/// <summary>
-		/// The bottom tee.
-		/// </summary>
-		public Rune BottomTee = '\u2534';
-
-		/// <summary>
-		/// Checkmark.
-		/// </summary>
-		public Rune Checked = '\u221a';
-
-		/// <summary>
-		/// Un-checked checkmark.
-		/// </summary>
-		public Rune UnChecked = '\u2574';
-
-		/// <summary>
-		/// Null-checked checkmark.
-		/// </summary>
-		public Rune NullChecked = '\u2370';
-
-		/// <summary>
-		/// Selected mark.
-		/// </summary>
-		public Rune Selected = '\u25cf';
-
-		/// <summary>
-		/// Un-selected selected mark.
-		/// </summary>
-		public Rune UnSelected = '\u25cc';
-
-		/// <summary>
-		/// Right Arrow.
-		/// </summary>
-		public Rune RightArrow = '\u25ba';
-
-		/// <summary>
-		/// Left Arrow.
-		/// </summary>
-		public Rune LeftArrow = '\u25c4';
-
-		/// <summary>
-		/// Down Arrow.
-		/// </summary>
-		public Rune DownArrow = '\u25bc';
-
-		/// <summary>
-		/// Up Arrow.
-		/// </summary>
-		public Rune UpArrow = '\u25b2';
-
-		/// <summary>
-		/// Left indicator for default action (e.g. for <see cref="Button"/>).
-		/// </summary>
-		public Rune LeftDefaultIndicator = '\u25e6';
-
-		/// <summary>
-		/// Right indicator for default action (e.g. for <see cref="Button"/>).
-		/// </summary>
-		public Rune RightDefaultIndicator = '\u25e6';
-
-		/// <summary>
-		/// Left frame/bracket (e.g. '[' for <see cref="Button"/>).
-		/// </summary>
-		public Rune LeftBracket = '[';
-
-		/// <summary>
-		/// Right frame/bracket (e.g. ']' for <see cref="Button"/>).
-		/// </summary>
-		public Rune RightBracket = ']';
-
-		/// <summary>
-		/// Blocks Segment indicator for meter views (e.g. <see cref="ProgressBar"/>.
-		/// </summary>
-		public Rune BlocksMeterSegment = '\u258c';
-
-		/// <summary>
-		/// Continuous Segment indicator for meter views (e.g. <see cref="ProgressBar"/>.
-		/// </summary>
-		public Rune ContinuousMeterSegment = '\u2588';
-
-		/// <summary>
-		/// Horizontal double line character.
-		/// </summary>
-		public Rune HDbLine = '\u2550';
-
-		/// <summary>
-		/// Vertical double line character.
-		/// </summary>
-		public Rune VDbLine = '\u2551';
-
-		/// <summary>
-		/// Upper left double corner
-		/// </summary>
-		public Rune ULDbCorner = '\u2554';
-
-		/// <summary>
-		/// Lower left double corner
-		/// </summary>
-		public Rune LLDbCorner = '\u255a';
-
-		/// <summary>
-		/// Upper right double corner
-		/// </summary>
-		public Rune URDbCorner = '\u2557';
-
-		/// <summary>
-		/// Lower right double corner
-		/// </summary>
-		public Rune LRDbCorner = '\u255d';
-
-		/// <summary>
-		/// Upper left rounded corner
-		/// </summary>
-		public Rune ULRCorner = '\u256d';
-
-		/// <summary>
-		/// Lower left rounded corner
-		/// </summary>
-		public Rune LLRCorner = '\u2570';
-
-		/// <summary>
-		/// Upper right rounded corner
-		/// </summary>
-		public Rune URRCorner = '\u256e';
-
-		/// <summary>
-		/// Lower right rounded corner
-		/// </summary>
-		public Rune LRRCorner = '\u256f';
-
-		/// <summary>
-		/// Horizontal double dashed line character.
-		/// </summary>
-		public Rune HDsLine = '\u254c';
-
-		/// <summary>
-		/// Vertical triple dashed line character.
-		/// </summary>
-		public Rune VDsLine = '\u2506';
-
-		/// <summary>
-		/// Horizontal triple dashed line character.
-		/// </summary>
-		public Rune HDtLine = '\u2504';
-
-		/// <summary>
-		/// Horizontal quadruple dashed line character.
-		/// </summary>
-		public Rune HD4Line = '\u2508';
-
-		/// <summary>
-		/// Vertical double dashed line character.
-		/// </summary>
-		public Rune VD2Line = '\u254e';
-
-		/// <summary>
-		/// Vertical quadruple dashed line character.
-		/// </summary>
-		public Rune VDtLine = '\u250a';
-
-		/// <summary>
-		/// Horizontal heavy line character.
-		/// </summary>
-		public Rune HThLine = '\u2501';
-
-		/// <summary>
-		/// Vertical heavy line character.
-		/// </summary>
-		public Rune VThLine = '\u2503';
-
-		/// <summary>
-		/// Upper left heavy corner
-		/// </summary>
-		public Rune ULThCorner = '\u250f';
-
-		/// <summary>
-		/// Lower left heavy corner
-		/// </summary>
-		public Rune LLThCorner = '\u2517';
-
-		/// <summary>
-		/// Upper right heavy corner
-		/// </summary>
-		public Rune URThCorner = '\u2513';
-
-		/// <summary>
-		/// Lower right heavy corner
-		/// </summary>
-		public Rune LRThCorner = '\u251b';
-
-		/// <summary>
-		/// Horizontal heavy double dashed line character.
-		/// </summary>
-		public Rune HThDsLine = '\u254d';
-
-		/// <summary>
-		/// Vertical heavy triple dashed line character.
-		/// </summary>
-		public Rune VThDsLine = '\u2507';
-
-		/// <summary>
-		/// Horizontal heavy triple dashed line character.
-		/// </summary>
-		public Rune HThDtLine = '\u2505';
-
-		/// <summary>
-		/// Horizontal heavy quadruple dashed line character.
-		/// </summary>
-		public Rune HThD4Line = '\u2509';
-
-		/// <summary>
-		/// Vertical heavy double dashed line character.
-		/// </summary>
-		public Rune VThD2Line = '\u254f';
-
-		/// <summary>
-		/// Vertical heavy quadruple dashed line character.
-		/// </summary>
-		public Rune VThDtLine = '\u250b';
-
-		/// <summary>
-		/// The left half line.
-		/// </summary>
-		public Rune HalfLeftLine = '\u2574';
-
-		/// <summary>
-		/// The up half line.
-		/// </summary>
-		public Rune HalfTopLine = '\u2575';
-
-		/// <summary>
-		/// The right half line.
-		/// </summary>
-		public Rune HalfRightLine = '\u2576';
-
-		/// <summary>
-		/// The down half line.
-		/// </summary>
-		public Rune HalfBottomLine = '\u2577';
-
-		/// <summary>
-		/// The heavy left half line.
-		/// </summary>
-		public Rune ThHalfLeftLine = '\u2578';
-
-		/// <summary>
-		/// The heavy up half line.
-		/// </summary>
-		public Rune ThHalfTopLine = '\u2579';
-
-		/// <summary>
-		/// The heavy right half line.
-		/// </summary>
-		public Rune ThHalfRightLine = '\u257a';
-
-		/// <summary>
-		/// The heavy light down half line.
-		/// </summary>
-		public Rune ThHalfBottomLine = '\u257b';
-
-		/// <summary>
-		/// The light left and heavy right line.
-		/// </summary>
-		public Rune ThRightSideLine = '\u257c';
-
-		/// <summary>
-		/// The light up and heavy down line.
-		/// </summary>
-		public Rune ThBottomSideLine = '\u257d';
-
-		/// <summary>
-		/// The heavy left and light right line.
-		/// </summary>
-		public Rune ThLeftSideLine = '\u257e';
-
-		/// <summary>
-		/// The heavy up and light down line.
-		/// </summary>
-		public Rune ThTopSideLine = '\u257f';
-
 		private Attribute currentAttribute;
 
 		/// <summary>

+ 21 - 10
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -7,7 +7,7 @@ using System.Diagnostics;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading.Tasks;
-using NStack;
+using System.Text;
 using Unix.Terminal;
 
 namespace Terminal.Gui {
@@ -48,10 +48,21 @@ namespace Terminal.Gui {
 		}
 
 		static bool sync = false;
+
+		public override bool IsRuneSupported (Rune rune)
+		{
+			// See Issue #2615 - CursesDriver is broken with non-BMP characters
+			return base.IsRuneSupported (rune) && rune.IsBmp;
+		}
+
 		public override void AddRune (Rune rune)
 		{
-			rune = MakePrintable (rune);
-			var runeWidth = Rune.ColumnWidth (rune);
+			if (!IsRuneSupported (rune)) {
+				rune = Rune.ReplacementChar;
+			}
+
+			rune = rune.MakePrintable ();
+			var runeWidth = rune.GetColumns ();
 			var validClip = IsValidContent (ccol, crow, Clip);
 
 			if (validClip) {
@@ -61,7 +72,7 @@ namespace Terminal.Gui {
 				}
 				if (runeWidth == 0 && ccol > 0) {
 					var r = contents [crow, ccol - 1, 0];
-					var s = new string (new char [] { (char)r, (char)rune });
+					var s = new string (new char [] { (char)r, (char)rune.Value });
 					string sn;
 					if (!s.IsNormalized ()) {
 						sn = s.Normalize ();
@@ -76,7 +87,7 @@ namespace Terminal.Gui {
 
 				} else {
 					if (runeWidth < 2 && ccol > 0
-						&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
+						&& ((Rune)(char)contents [crow, ccol - 1, 0]).GetColumns () > 1) {
 
 						var curAtttib = CurrentAttribute;
 						Curses.attrset (contents [crow, ccol - 1, 1]);
@@ -86,7 +97,7 @@ namespace Terminal.Gui {
 						Curses.attrset (curAtttib);
 
 					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-						&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+						&& ((Rune)(char)contents [crow, ccol, 0]).GetColumns () > 1) {
 
 						var curAtttib = CurrentAttribute;
 						Curses.attrset (contents [crow, ccol + 1, 1]);
@@ -100,8 +111,8 @@ namespace Terminal.Gui {
 						Curses.addch ((int)(uint)' ');
 						contents [crow, ccol, 0] = (int)(uint)' ';
 					} else {
-						Curses.addch ((int)(uint)rune);
-						contents [crow, ccol, 0] = (int)(uint)rune;
+						Curses.addch ((int)(uint)rune.Value);
+						contents [crow, ccol, 0] = (int)(uint)rune.Value;
 					}
 					contents [crow, ccol, 1] = CurrentAttribute;
 					contents [crow, ccol, 2] = 1;
@@ -127,10 +138,10 @@ namespace Terminal.Gui {
 			}
 		}
 
-		public override void AddStr (ustring str)
+		public override void AddStr (string str)
 		{
 			// TODO; optimize this to determine if the str fits in the clip region, and if so, use Curses.addstr directly
-			foreach (var rune in str)
+			foreach (var rune in str.EnumerateRunes ())
 				AddRune (rune);
 		}
 

+ 2 - 1
Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs

@@ -82,6 +82,7 @@ namespace Terminal.Gui {
 		bool poll_dirty = true;
 		int [] wakeupPipes = new int [2];
 		static IntPtr ignore = Marshal.AllocHGlobal (1);
+		static IntPtr readHandle = Marshal.AllocHGlobal (1);
 		MainLoop mainLoop;
 		bool winChanged;
 
@@ -97,7 +98,7 @@ namespace Terminal.Gui {
 			this.mainLoop = mainLoop;
 			pipe (wakeupPipes);
 			AddWatch (wakeupPipes [0], Condition.PollIn, ml => {
-				read (wakeupPipes [0], ignore, (IntPtr)1);
+				read (wakeupPipes [0], ignore, readHandle);
 				return true;
 			});
 		}

+ 0 - 2
Terminal.Gui/ConsoleDrivers/CursesDriver/UnmanagedLibrary.cs

@@ -256,7 +256,6 @@ namespace Unix.Terminal {
 		/// to avoid the dependency on libc-dev Linux.
 		/// </summary>
 		static class CoreCLR {
-#if NET6_0
 			// Custom resolver to support true single-file apps
 			// (those which run directly from bundle; in-memory).
 			//	 -1 on Unix means self-referencing binary (libcoreclr.so)
@@ -265,7 +264,6 @@ namespace Unix.Terminal {
 			static CoreCLR() =>  NativeLibrary.SetDllImportResolver(typeof(CoreCLR).Assembly,
 				(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) =>
 					libraryName == "libcoreclr.so" ? (IntPtr)(-1) : IntPtr.Zero);
-#endif
 
 			[DllImport ("libcoreclr.so")]
 			internal static extern IntPtr dlopen (string filename, int flags);

+ 12 - 12
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -7,7 +7,7 @@ using System.Diagnostics;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading;
-using NStack;
+using System.Text;
 
 // Alias Console to MockConsole so we don't accidentally use Console
 using Console = Terminal.Gui.FakeConsole;
@@ -118,8 +118,8 @@ namespace Terminal.Gui {
 
 		public override void AddRune (Rune rune)
 		{
-			rune = MakePrintable (rune);
-			var runeWidth = Rune.ColumnWidth (rune);
+			rune = rune.MakePrintable ();
+			var runeWidth = rune.GetColumns ();
 			var validClip = IsValidContent (ccol, crow, Clip);
 
 			if (validClip) {
@@ -130,7 +130,7 @@ namespace Terminal.Gui {
 				}
 				if (runeWidth == 0 && ccol > 0) {
 					var r = contents [crow, ccol - 1, 0];
-					var s = new string (new char [] { (char)r, (char)rune });
+					var s = new string (new char [] { (char)r, (char)rune.Value });
 					string sn;
 					if (!s.IsNormalized ()) {
 						sn = s.Normalize ();
@@ -144,12 +144,12 @@ namespace Terminal.Gui {
 
 				} else {
 					if (runeWidth < 2 && ccol > 0
-					&& Rune.ColumnWidth ((Rune)contents [crow, ccol - 1, 0]) > 1) {
+					&& ((Rune)contents [crow, ccol - 1, 0]).GetColumns () > 1) {
 
 						contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
 					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-						&& Rune.ColumnWidth ((Rune)contents [crow, ccol, 0]) > 1) {
+						&& ((Rune)contents [crow, ccol, 0]).GetColumns () > 1) {
 
 						contents [crow, ccol + 1, 0] = (int)(uint)' ';
 						contents [crow, ccol + 1, 2] = 1;
@@ -158,7 +158,7 @@ namespace Terminal.Gui {
 					if (runeWidth > 1 && ccol == Clip.Right - 1) {
 						contents [crow, ccol, 0] = (int)(uint)' ';
 					} else {
-						contents [crow, ccol, 0] = (int)(uint)rune;
+						contents [crow, ccol, 0] = (int)(uint)rune.Value;
 					}
 					contents [crow, ccol, 1] = CurrentAttribute;
 					contents [crow, ccol, 2] = 1;
@@ -191,9 +191,9 @@ namespace Terminal.Gui {
 			}
 		}
 
-		public override void AddStr (ustring str)
+		public override void AddStr (string str)
 		{
-			foreach (var rune in str)
+			foreach (var rune in str.EnumerateRunes ())
 				AddRune (rune);
 		}
 
@@ -281,11 +281,11 @@ namespace Terminal.Gui {
 						if (color != redrawColor)
 							SetColor (color);
 
-						Rune rune = contents [row, col, 0];
-						if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
+						Rune rune = (Rune)contents [row, col, 0];
+						if (rune.DecodeSurrogatePair (out char [] spair)) {
 							FakeConsole.Write (spair);
 						} else {
-							FakeConsole.Write ((char)rune);
+							FakeConsole.Write ((char)rune.Value);
 						}
 						contents [row, col, 2] = 0;
 					}

+ 24 - 15
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -11,7 +11,7 @@ using System.Linq;
 using System.Runtime.InteropServices;
 using System.Threading;
 using System.Threading.Tasks;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	internal class NetWinVTConsole {
@@ -195,12 +195,13 @@ namespace Terminal.Gui {
 						isEscSeq = false;
 						break;
 					}
-				} else if (consoleKeyInfo.KeyChar == (char)Key.Esc && isEscSeq) {
+				} else if (consoleKeyInfo.KeyChar == (char)Key.Esc && isEscSeq && cki != null) {
 					DecodeEscSeq (ref newConsoleKeyInfo, ref key, cki, ref mod);
 					cki = null;
 					break;
 				} else {
 					GetConsoleInputType (consoleKeyInfo);
+					isEscSeq = false;
 					break;
 				}
 			}
@@ -668,14 +669,14 @@ namespace Terminal.Gui {
 			if (contents.Length != Rows * Cols * 3) {
 				return;
 			}
-			rune = MakePrintable (rune);
-			var runeWidth = Rune.ColumnWidth (rune);
+			rune = rune.MakePrintable ();
+			var runeWidth = rune.GetColumns ();
 			var validClip = IsValidContent (ccol, crow, Clip);
 
 			if (validClip) {
 				if (runeWidth == 0 && ccol > 0) {
 					var r = contents [crow, ccol - 1, 0];
-					var s = new string (new char [] { (char)r, (char)rune });
+					var s = new string (new char [] { (char)r, (char)rune.Value });
 					string sn;
 					if (!s.IsNormalized ()) {
 						sn = s.Normalize ();
@@ -689,12 +690,12 @@ namespace Terminal.Gui {
 
 				} else {
 					if (runeWidth < 2 && ccol > 0
-						&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
+						&& ((Rune)(char)contents [crow, ccol - 1, 0]).GetColumns () > 1) {
 
 						contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
 					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-						&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+						&& ((Rune)(char)contents [crow, ccol, 0]).GetColumns () > 1) {
 
 						contents [crow, ccol + 1, 0] = (int)(uint)' ';
 						contents [crow, ccol + 1, 2] = 1;
@@ -703,7 +704,7 @@ namespace Terminal.Gui {
 					if (runeWidth > 1 && ccol == Clip.Right - 1) {
 						contents [crow, ccol, 0] = (int)(uint)' ';
 					} else {
-						contents [crow, ccol, 0] = (int)(uint)rune;
+						contents [crow, ccol, 0] = (int)(uint)rune.Value;
 					}
 					contents [crow, ccol, 1] = CurrentAttribute;
 					contents [crow, ccol, 2] = 1;
@@ -729,9 +730,9 @@ namespace Terminal.Gui {
 			}
 		}
 
-		public override void AddStr (ustring str)
+		public override void AddStr (string str)
 		{
-			foreach (var rune in str)
+			foreach (var rune in str.EnumerateRunes ())
 				AddRune (rune);
 		}
 
@@ -908,7 +909,8 @@ namespace Terminal.Gui {
 			int redrawAttr = -1;
 			var lastCol = -1;
 
-			Console.CursorVisible = false;
+			GetCursorVisibility (out CursorVisibility savedVisibitity);
+			SetCursorVisibility (CursorVisibility.Invisible);
 
 			for (int row = top; row < rows; row++) {
 				if (Console.WindowHeight < 1) {
@@ -950,12 +952,12 @@ namespace Terminal.Gui {
 							output.Append (WriteAttributes (attr));
 						}
 						outputWidth++;
-						var rune = contents [row, col, 0];
+						var rune = (Rune)contents [row, col, 0];
 						char [] spair;
-						if (Rune.DecodeSurrogatePair ((uint)rune, out spair)) {
+						if (rune.DecodeSurrogatePair (out spair)) {
 							output.Append (spair);
 						} else {
-							output.Append ((char)rune);
+							output.Append ((char)rune.Value);
 						}
 						contents [row, col, 2] = 0;
 					}
@@ -966,6 +968,7 @@ namespace Terminal.Gui {
 				}
 			}
 			SetCursorPosition (0, 0);
+			SetCursorVisibility (savedVisibitity);
 		}
 
 		void SetVirtualCursorPosition (int col, int row)
@@ -1443,7 +1446,13 @@ namespace Terminal.Gui {
 		public override bool SetCursorVisibility (CursorVisibility visibility)
 		{
 			savedCursorVisibility = visibility;
-			return Console.CursorVisible = visibility == CursorVisibility.Default;
+			var isVisible = Console.CursorVisible = visibility == CursorVisibility.Default;
+			if (isVisible) {
+				Console.Out.Write ("\x1b[?25h");
+			} else {
+				Console.Out.Write ("\x1b[?25l");
+			}
+			return isVisible;
 		}
 
 		/// <inheritdoc/>

+ 36 - 25
Terminal.Gui/ConsoleDrivers/WindowsDriver.cs

@@ -1,7 +1,7 @@
 //
 // WindowsDriver.cs: Windows specific driver
 //
-using NStack;
+using System.Text;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -742,7 +742,7 @@ namespace Terminal.Gui {
 
 			mLoop.ProcessInput = (e) => ProcessInput (e);
 
-			mLoop.WinChanged = (s,e) => {
+			mLoop.WinChanged = (s, e) => {
 				ChangeWin (e.Size);
 			};
 		}
@@ -902,6 +902,7 @@ namespace Terminal.Gui {
 				break;
 
 			case WindowsConsole.EventType.Focus:
+				keyModifiers = null;
 				break;
 			}
 		}
@@ -1518,17 +1519,27 @@ namespace Terminal.Gui {
 			return crow * Cols + ccol;
 		}
 
+		public override bool IsRuneSupported (Rune rune)
+		{
+			// See Issue #2610
+			return base.IsRuneSupported (rune) && rune.IsBmp;
+		}
+
 		public override void AddRune (Rune rune)
 		{
-			rune = MakePrintable (rune);
-			var runeWidth = Rune.ColumnWidth (rune);
+			if (!IsRuneSupported (rune)) {
+				rune = Rune.ReplacementChar;
+			}
+
+			rune = rune.MakePrintable ();
+			var runeWidth = rune.GetColumns ();
 			var position = GetOutputBufferPosition ();
 			var validClip = IsValidContent (ccol, crow, Clip);
 
 			if (validClip) {
 				if (runeWidth == 0 && ccol > 0) {
 					var r = contents [crow, ccol - 1, 0];
-					var s = new string (new char [] { (char)r, (char)rune });
+					var s = new string (new char [] { (char)r, (char)rune.Value });
 					string sn;
 					if (!s.IsNormalized ()) {
 						sn = s.Normalize ();
@@ -1545,14 +1556,14 @@ namespace Terminal.Gui {
 					WindowsConsole.SmallRect.Update (ref damageRegion, (short)(ccol - 1), (short)crow);
 				} else {
 					if (runeWidth < 2 && ccol > 0
-						&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
+						&& ((Rune)(char)contents [crow, ccol - 1, 0]).GetColumns () > 1) {
 
 						var prevPosition = crow * Cols + (ccol - 1);
 						OutputBuffer [prevPosition].Char.UnicodeChar = ' ';
 						contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
 					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-						&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+						&& ((Rune)(char)contents [crow, ccol, 0]).GetColumns () > 1) {
 
 						var prevPosition = GetOutputBufferPosition () + 1;
 						OutputBuffer [prevPosition].Char.UnicodeChar = (char)' ';
@@ -1563,8 +1574,8 @@ namespace Terminal.Gui {
 						OutputBuffer [position].Char.UnicodeChar = (char)' ';
 						contents [crow, ccol, 0] = (int)(uint)' ';
 					} else {
-						OutputBuffer [position].Char.UnicodeChar = (char)rune;
-						contents [crow, ccol, 0] = (int)(uint)rune;
+						OutputBuffer [position].Char.UnicodeChar = (char)rune.Value;
+						contents [crow, ccol, 0] = (int)(uint)rune.Value;
 					}
 					OutputBuffer [position].Attributes = (ushort)CurrentAttribute;
 					contents [crow, ccol, 1] = CurrentAttribute;
@@ -1594,9 +1605,9 @@ namespace Terminal.Gui {
 			}
 		}
 
-		public override void AddStr (ustring str)
+		public override void AddStr (string str)
 		{
-			foreach (var rune in str)
+			foreach (var rune in str.EnumerateRunes ())
 				AddRune (rune);
 		}
 
@@ -1868,7 +1879,7 @@ namespace Terminal.Gui {
 		/// Invoked when the window is changed.
 		/// </summary>
 		public EventHandler<SizeChangedEventArgs> WinChanged;
-		
+
 		public WindowsMainLoop (ConsoleDriver consoleDriver = null)
 		{
 			this.consoleDriver = consoleDriver ?? throw new ArgumentNullException ("Console driver instance must be provided.");
@@ -1990,7 +2001,7 @@ namespace Terminal.Gui {
 			}
 			if (winChanged) {
 				winChanged = false;
-				WinChanged?.Invoke (this, new SizeChangedEventArgs(windowSize));
+				WinChanged?.Invoke (this, new SizeChangedEventArgs (windowSize));
 			}
 		}
 	}
@@ -2005,34 +2016,34 @@ namespace Terminal.Gui {
 
 		protected override string GetClipboardDataImpl ()
 		{
-			//if (!IsClipboardFormatAvailable (cfUnicodeText))
-			//	return null;
-
 			try {
-				if (!OpenClipboard (IntPtr.Zero))
-					return null;
+				if (!OpenClipboard (IntPtr.Zero)) {
+					return string.Empty;
+				}
 
 				IntPtr handle = GetClipboardData (cfUnicodeText);
-				if (handle == IntPtr.Zero)
-					return null;
+				if (handle == IntPtr.Zero) {
+					return string.Empty;
+				}
 
 				IntPtr pointer = IntPtr.Zero;
 
 				try {
 					pointer = GlobalLock (handle);
-					if (pointer == IntPtr.Zero)
-						return null;
+					if (pointer == IntPtr.Zero) {
+						return string.Empty;
+					}
 
 					int size = GlobalSize (handle);
 					byte [] buff = new byte [size];
 
 					Marshal.Copy (pointer, buff, 0, size);
 
-					return System.Text.Encoding.Unicode.GetString (buff)
-						.TrimEnd ('\0');
+					return Encoding.Unicode.GetString (buff).TrimEnd ('\0');
 				} finally {
-					if (pointer != IntPtr.Zero)
+					if (pointer != IntPtr.Zero) {
 						GlobalUnlock (handle);
+					}
 				}
 			} finally {
 				CloseClipboard ();

+ 20 - 0
Terminal.Gui/Drawing/Cell.cs

@@ -0,0 +1,20 @@
+using System.Text;
+
+
+namespace Terminal.Gui; 
+
+/// <summary>
+/// Represents a single row/column within the <see cref="LineCanvas"/>. Includes the glyph and the foreground/background colors.
+/// </summary>
+public class Cell {
+	/// <summary>
+	/// The glyph to draw.
+	/// </summary>
+	public Rune? Rune { get; set; }
+
+	/// <summary>
+	/// The foreground color to draw the glyph with.
+	/// </summary>
+	public Attribute? Attribute { get; set; }
+
+}

+ 670 - 0
Terminal.Gui/Drawing/Glyphs.cs

@@ -0,0 +1,670 @@
+using static Terminal.Gui.ConfigurationManager;
+using System.Text.Json.Serialization;
+using System.Text;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Defines the standard set of glyphs used to draw checkboxes, lines, borders, etc...
+	/// </summary>
+	/// <remarks>
+	/// <para>
+	/// Access with <see cref="CM.Glyphs"/> (which is a global using alias for <see cref="ConfigurationManager.Glyphs"/>).
+	/// </para>
+	/// <para>
+	/// The default glyphs can be changed via the <see cref="ConfigurationManager"/>. Within a <c>config.json</c> file 
+	/// The Json property name is the property name prefixed with "Glyphs.". 
+	/// </para>
+	/// <para>
+	/// The JSon property can be either a decimal number or a string. The string may be one of:
+	/// - A unicode char (e.g. "☑")
+	/// - A hex value in U+ format (e.g. "U+2611")
+	/// - A hex value in UTF-16 format (e.g. "\\u2611")
+	/// </para>
+	/// </remarks>
+	public class GlyphDefinitions {
+		#region ----------------- Single Glyphs -----------------
+
+		/// <summary>
+		/// Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).
+		/// </summary>
+		public Rune Checked { get; set; } = (Rune)'☑';
+
+		/// <summary>
+		/// Not Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).
+		/// </summary>
+		public Rune UnChecked { get; set; } = (Rune)'☐';
+
+		/// <summary>
+		/// Null Checked indicator (e.g. for <see cref="ListView"/> and <see cref="CheckBox"/>).
+		/// </summary>
+		public Rune NullChecked { get; set; } = (Rune)'☒';
+
+		/// <summary>
+		/// Selected indicator  (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).
+		/// </summary>
+		public Rune Selected { get; set; } = (Rune)'◉';
+
+		/// <summary>
+		/// Not Selected indicator (e.g. for <see cref="ListView"/> and <see cref="RadioGroup"/>).
+		/// </summary>
+		public Rune UnSelected { get; set; } = (Rune)'○';
+
+		/// <summary>
+		/// Horizontal arrow.
+		/// </summary>
+		public Rune RightArrow { get; set; } = (Rune)'►';
+
+		/// <summary>
+		/// Left arrow.
+		/// </summary>
+		public Rune LeftArrow { get; set; } = (Rune)'◄';
+
+		/// <summary>
+		/// Down arrow.
+		/// </summary>
+		public Rune DownArrow { get; set; } = (Rune)'▼';
+
+		/// <summary>
+		/// Vertical arrow.
+		/// </summary>
+		public Rune UpArrow { get; set; } = (Rune)'▲';
+
+		/// <summary>
+		/// Left default indicator (e.g. for <see cref="Button"/>.
+		/// </summary>
+		public Rune LeftDefaultIndicator { get; set; } = (Rune)'►';
+
+		/// <summary>
+		/// Horizontal default indicator (e.g. for <see cref="Button"/>.
+		/// </summary>
+		public Rune RightDefaultIndicator { get; set; } = (Rune)'◄';
+
+		/// <summary>
+		/// Left Bracket (e.g. for <see cref="Button"/>. Default is (U+005B) - [.
+		/// </summary>
+		public Rune LeftBracket { get; set; } = (Rune)'⟦';
+
+		/// <summary>
+		/// Horizontal Bracket (e.g. for <see cref="Button"/>. Default is (U+005D) - ].
+		/// </summary>
+		public Rune RightBracket { get; set; } = (Rune)'⟧';
+
+		/// <summary>
+		/// Half block meter segment (e.g. for <see cref="ProgressBar"/>).
+		/// </summary>
+		public Rune BlocksMeterSegment { get; set; } = (Rune)'▌';
+
+		/// <summary>
+		/// Continuous block meter segment (e.g. for <see cref="ProgressBar"/>).
+		/// </summary>
+		public Rune ContinuousMeterSegment { get; set; } = (Rune)'█';
+
+		/// <summary>
+		/// Stipple pattern (e.g. for <see cref="ScrollBarView"/>). Default is Light Shade (U+2591) - ░.
+		/// </summary>
+		public Rune Stipple { get; set; } = (Rune)'░';
+
+		/// <summary>
+		/// Diamond (e.g. for <see cref="ScrollBarView"/>. Default is Lozenge (U+25CA) - ◊.
+		/// </summary>
+		public Rune Diamond { get; set; } = (Rune)'◊';
+
+		/// <summary>
+		/// Close. Default is Heavy Ballot X (U+2718) - ✘.
+		/// </summary>
+		public Rune Close { get; set; } = (Rune)'✘';
+
+		/// <summary>
+		/// Minimize. Default is Lower Horizontal Shadowed White Circle (U+274F) - ❏.
+		/// </summary>
+		public Rune Minimize { get; set; } = (Rune)'❏';
+
+		/// <summary>
+		/// Maximize. Default is Upper Horizontal Shadowed White Circle (U+273D) - ✽.
+		/// </summary>
+		public Rune Maximize { get; set; } = (Rune)'✽';
+
+		/// <summary>
+		/// Dot. Default is (U+2219) - ∙.
+		/// </summary>
+		public Rune Dot { get; set; } = (Rune)'∙';
+
+		/// <summary>
+		/// Expand (e.g. for <see cref="TreeView"/>.
+		/// </summary>
+		public Rune Expand { get; set; } = (Rune)'+';
+
+		/// <summary>
+		/// Expand (e.g. for <see cref="TreeView"/>.
+		/// </summary>
+		public Rune Collapse { get; set; } = (Rune)'-';
+
+		/// <summary>
+		/// Apple (non-BMP). Because snek. And because it's an example of a non-BMP surrogate pair. See Issue #2610.
+		/// </summary>
+		public Rune Apple { get; set; } = "🍎".ToRunes () [0]; // nonBMP
+
+		/// <summary>
+		/// Apple (BMP). Because snek. See Issue #2610.
+		/// </summary>
+		public Rune AppleBMP { get; set; } = (Rune)'❦';
+
+		///// <summary>
+		///// A nonprintable (low surrogate) that should fail to ctor.
+		///// </summary>
+		//public Rune InvalidGlyph { get; set; } = (Rune)'\ud83d';
+
+		#endregion
+
+		/// <summary>
+		/// Folder icon.  Defaults to ꤉ (Kayah Li Digit Nine)
+		/// </summary>
+		public Rune Folder { get; set; } = (Rune)'꤉';
+
+		/// <summary>
+		/// File icon.  Defaults to ☰ (Trigram For Heaven)
+		/// </summary>
+		public Rune File { get; set; } = (Rune)'☰';
+
+		#region ----------------- Lines -----------------
+		/// <summary>
+		/// Box Drawings Horizontal Line - Light (U+2500) - ─
+		/// </summary>
+		public Rune HLine { get; set; } = (Rune)'─';
+
+		/// <summary>
+		/// Box Drawings Vertical Line - Light (U+2502) - │
+		/// </summary>
+		public Rune VLine { get; set; } = (Rune)'│';
+
+		/// <summary>
+		/// Box Drawings Double Horizontal (U+2550) - ═
+		/// </summary>
+		public Rune HLineDbl { get; set; } = (Rune)'═';
+
+		/// <summary>
+		/// Box Drawings Double Vertical (U+2551) - ║
+		/// </summary>
+		public Rune VLineDbl { get; set; } = (Rune)'║';
+
+		/// <summary>
+		/// Box Drawings Heavy Double Dash Horizontal (U+254D) - ╍
+		/// </summary>
+		public Rune HLineHvDa2 { get; set; } = (Rune)'╍';
+
+		/// <summary>
+		/// Box Drawings Heavy Triple Dash Vertical (U+2507) - ┇
+		/// </summary>
+		public Rune VLineHvDa3 { get; set; } = (Rune)'┇';
+
+		/// <summary>
+		/// Box Drawings Heavy Triple Dash Horizontal (U+2505) - ┅
+		/// </summary>
+		public Rune HLineHvDa3 { get; set; } = (Rune)'┅';
+
+		/// <summary>
+		/// Box Drawings Heavy Quadruple Dash Horizontal (U+2509) - ┉
+		/// </summary>
+		public Rune HLineHvDa4 { get; set; } = (Rune)'┉';
+
+		/// <summary>
+		/// Box Drawings Heavy Double Dash Vertical (U+254F) - ╏
+		/// </summary>
+		public Rune VLineHvDa2 { get; set; } = (Rune)'╏';
+
+		/// <summary>
+		/// Box Drawings Heavy Quadruple Dash Vertical (U+250B) - ┋
+		/// </summary>
+		public Rune VLineHvDa4 { get; set; } = (Rune)'┋';
+
+		/// <summary>
+		/// Box Drawings Light Double Dash Horizontal (U+254C) - ╌
+		/// </summary>
+		public Rune HLineDa2 { get; set; } = (Rune)'╌';
+
+		/// <summary>
+		/// Box Drawings Light Triple Dash Vertical (U+2506) - ┆
+		/// </summary>
+		public Rune VLineDa3 { get; set; } = (Rune)'┆';
+
+		/// <summary>
+		/// Box Drawings Light Triple Dash Horizontal (U+2504) - ┄
+		/// </summary>
+		public Rune HLineDa3 { get; set; } = (Rune)'┄';
+
+		/// <summary>
+		/// Box Drawings Light Quadruple Dash Horizontal (U+2508) - ┈
+		/// </summary>
+		public Rune HLineDa4 { get; set; } = (Rune)'┈';
+
+		/// <summary>
+		/// Box Drawings Light Double Dash Vertical (U+254E) - ╎
+		/// </summary>
+		public Rune VLineDa2 { get; set; } = (Rune)'╎';
+
+		/// <summary>
+		/// Box Drawings Light Quadruple Dash Vertical (U+250A) - ┊
+		/// </summary>
+		public Rune VLineDa4 { get; set; } = (Rune)'┊';
+
+		/// <summary>
+		/// Box Drawings Heavy Horizontal (U+2501) - ━
+		/// </summary>
+		public Rune HLineHv { get; set; } = (Rune)'━';
+
+		/// <summary>
+		/// Box Drawings Heavy Vertical (U+2503) - ┃
+		/// </summary>
+		public Rune VLineHv { get; set; } = (Rune)'┃';
+
+		/// <summary>
+		/// Box Drawings Light Left (U+2574) - ╴
+		/// </summary>
+		public Rune HalfLeftLine { get; set; } = (Rune)'╴';
+
+		/// <summary>
+		/// Box Drawings Light Vertical (U+2575) - ╵
+		/// </summary>
+		public Rune HalfTopLine { get; set; } = (Rune)'╵';
+
+		/// <summary>
+		/// Box Drawings Light Horizontal (U+2576) - ╶
+		/// </summary>
+		public Rune HalfRightLine { get; set; } = (Rune)'╶';
+
+		/// <summary>
+		/// Box Drawings Light Down (U+2577) - ╷
+		/// </summary>
+		public Rune HalfBottomLine { get; set; } = (Rune)'╷';
+
+		/// <summary>
+		/// Box Drawings Heavy Left (U+2578) - ╸
+		/// </summary>
+		public Rune HalfLeftLineHv { get; set; } = (Rune)'╸';
+
+		/// <summary>
+		/// Box Drawings Heavy Vertical (U+2579) - ╹
+		/// </summary>
+		public Rune HalfTopLineHv { get; set; } = (Rune)'╹';
+
+		/// <summary>
+		/// Box Drawings Heavy Horizontal (U+257A) - ╺
+		/// </summary>
+		public Rune HalfRightLineHv { get; set; } = (Rune)'╺';
+
+		/// <summary>
+		/// Box Drawings Light Vertical and Horizontal (U+257B) - ╻
+		/// </summary>
+		public Rune HalfBottomLineLt { get; set; } = (Rune)'╻';
+
+		/// <summary>
+		/// Box Drawings Light Horizontal and Heavy Horizontal (U+257C) - ╼
+		/// </summary>
+		public Rune RightSideLineLtHv { get; set; } = (Rune)'╼';
+
+		/// <summary>
+		/// Box Drawings Light Vertical and Heavy Horizontal (U+257D) - ╽
+		/// </summary>
+		public Rune BottomSideLineLtHv { get; set; } = (Rune)'╽';
+
+		/// <summary>
+		/// Box Drawings Heavy Left and Light Horizontal (U+257E) - ╾
+		/// </summary>
+		public Rune LeftSideLineHvLt { get; set; } = (Rune)'╾';
+
+		/// <summary>
+		/// Box Drawings Heavy Vertical and Light Horizontal (U+257F) - ╿
+		/// </summary>
+		public Rune TopSideLineHvLt { get; set; } = (Rune)'╿';
+		#endregion
+
+		#region ----------------- Upper Left Corners -----------------
+		/// <summary>
+		/// Box Drawings Upper Left Corner - Light Vertical and Light Horizontal (U+250C) - ┌
+		/// </summary>
+		public Rune ULCorner { get; set; } = (Rune)'┌';
+
+		/// <summary>
+		/// Box Drawings Upper Left Corner -  Double (U+2554) - ╔
+		/// </summary>
+		public Rune ULCornerDbl { get; set; } = (Rune)'╔';
+
+		/// <summary>
+		/// Box Drawings Upper Left Corner - Light Arc Down and Horizontal (U+256D) - ╭
+		/// </summary>
+		public Rune ULCornerR { get; set; } = (Rune)'╭';
+
+		/// <summary>
+		/// Box Drawings Heavy Down and Horizontal (U+250F) - ┏
+		/// </summary>
+		public Rune ULCornerHv { get; set; } = (Rune)'┏';
+
+		/// <summary>
+		/// Box Drawings Down Heavy and Horizontal Light (U+251E) - ┎
+		/// </summary>
+		public Rune ULCornerHvLt { get; set; } = (Rune)'┎';
+
+		/// <summary>
+		/// Box Drawings Down Light and Horizontal Heavy (U+250D) - ┎
+		/// </summary>
+		public Rune ULCornerLtHv { get; set; } = (Rune)'┍';
+
+		/// <summary>
+		/// Box Drawings Double Down and Single Horizontal (U+2553) - ╓
+		/// </summary>
+		public Rune ULCornerDblSingle { get; set; } = (Rune)'╓';
+
+		/// <summary>
+		/// Box Drawings Single Down and Double Horizontal (U+2552) - ╒
+		/// </summary>
+		public Rune ULCornerSingleDbl { get; set; } = (Rune)'╒';
+		#endregion
+
+		#region ----------------- Lower Left Corners -----------------
+		/// <summary>
+		/// Box Drawings Lower Left Corner - Light Vertical and Light Horizontal (U+2514) - └
+		/// </summary>
+		public Rune LLCorner { get; set; } = (Rune)'└';
+
+		/// <summary>
+		/// Box Drawings Heavy Vertical and Horizontal (U+2517) - ┗
+		/// </summary>
+		public Rune LLCornerHv { get; set; } = (Rune)'┗';
+
+		/// <summary>
+		/// Box Drawings Heavy Vertical and Horizontal Light (U+2516) - ┖
+		/// </summary>
+		public Rune LLCornerHvLt { get; set; } = (Rune)'┖';
+
+		/// <summary>
+		/// Box Drawings Vertical Light and Horizontal Heavy (U+2511) - ┕
+		/// </summary>
+		public Rune LLCornerLtHv { get; set; } = (Rune)'┕';
+
+		/// <summary>
+		/// Box Drawings Double Vertical and Double Left (U+255A) - ╚
+		/// </summary>
+		public Rune LLCornerDbl { get; set; } = (Rune)'╚';
+
+		/// <summary>
+		/// Box Drawings Single Vertical and Double Left (U+2558) - ╘
+		/// </summary>
+		public Rune LLCornerSingleDbl { get; set; } = (Rune)'╘';
+
+		/// <summary>
+		/// Box Drawings Double Down and Single Left (U+2559) - ╙
+		/// </summary>
+		public Rune LLCornerDblSingle { get; set; } = (Rune)'╙';
+
+		/// <summary>
+		/// Box Drawings Upper Left Corner - Light Arc Down and Left (U+2570) - ╰
+		/// </summary>
+		public Rune LLCornerR { get; set; } = (Rune)'╰';
+
+		#endregion
+
+		#region ----------------- Upper Right Corners -----------------
+		/// <summary>
+		/// Box Drawings Upper Horizontal Corner - Light Vertical and Light Horizontal (U+2510) - ┐
+		/// </summary>
+		public Rune URCorner { get; set; } = (Rune)'┐';
+
+		/// <summary>
+		/// Box Drawings Upper Horizontal Corner - Double Vertical and Double Horizontal (U+2557) - ╗
+		/// </summary>
+		public Rune URCornerDbl { get; set; } = (Rune)'╗';
+
+		/// <summary>
+		/// Box Drawings Upper Horizontal Corner - Light Arc Vertical and Horizontal (U+256E) - ╮
+		/// </summary>
+		public Rune URCornerR { get; set; } = (Rune)'╮';
+
+		/// <summary>
+		/// Box Drawings Heavy Down and Left (U+2513) - ┓
+		/// </summary>
+		public Rune URCornerHv { get; set; } = (Rune)'┓';
+
+		/// <summary>
+		/// Box Drawings Heavy Vertical and Left Down Light (U+2511) - ┑
+		/// </summary>
+		public Rune URCornerHvLt { get; set; } = (Rune)'┑';
+
+		/// <summary>
+		/// Box Drawings Down Light and Horizontal Heavy (U+2514) - ┒
+		/// </summary>
+		public Rune URCornerLtHv { get; set; } = (Rune)'┒';
+
+		/// <summary>
+		/// Box Drawings Double Vertical and Single Left (U+2556) - ╖
+		/// </summary>
+		public Rune URCornerDblSingle { get; set; } = (Rune)'╖';
+
+		/// <summary>
+		/// Box Drawings Single Vertical and Double Left (U+2555) - ╕
+		/// </summary>
+		public Rune URCornerSingleDbl { get; set; } = (Rune)'╕';
+		#endregion
+
+		#region ----------------- Lower Right Corners -----------------
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Light (U+2518) - ┘
+		/// </summary>
+		public Rune LRCorner { get; set; } = (Rune)'┘';
+
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Double (U+255D) - ╝
+		/// </summary>
+		public Rune LRCornerDbl { get; set; } = (Rune)'╝';
+
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Rounded (U+256F) - ╯
+		/// </summary>
+		public Rune LRCornerR { get; set; } = (Rune)'╯';
+
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Heavy (U+251B) - ┛
+		/// </summary>
+		public Rune LRCornerHv { get; set; } = (Rune)'┛';
+
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Double Vertical and Single Horizontal (U+255C) - ╜
+		/// </summary>
+		public Rune LRCornerDblSingle { get; set; } = (Rune)'╜';
+
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Single Vertical and Double Horizontal (U+255B) - ╛
+		/// </summary>
+		public Rune LRCornerSingleDbl { get; set; } = (Rune)'╛';
+
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Light Vertical and Heavy Horizontal (U+2519) - ┙
+		/// </summary>
+		public Rune LRCornerLtHv { get; set; } = (Rune)'┙';
+
+		/// <summary>
+		/// Box Drawings Lower Right Corner - Heavy Vertical and Light Horizontal (U+251A) - ┚
+		/// </summary>
+		public Rune LRCornerHvLt { get; set; } = (Rune)'┚';
+		#endregion
+
+		#region ----------------- Tees -----------------
+		/// <summary>
+		/// Box Drawings Left Tee - Single Vertical and Single Horizontal (U+251C) - ├
+		/// </summary>
+		public Rune LeftTee { get; set; } = (Rune)'├';
+
+		/// <summary>
+		/// Box Drawings Left Tee - Single Vertical and Double Horizontal (U+255E) - ╞
+		/// </summary>
+		public Rune LeftTeeDblH { get; set; } = (Rune)'╞';
+
+		/// <summary>
+		/// Box Drawings Left Tee - Double Vertical and Single Horizontal (U+255F) - ╟
+		/// </summary>
+		public Rune LeftTeeDblV { get; set; } = (Rune)'╟';
+
+		/// <summary>
+		/// Box Drawings Left Tee - Double Vertical and Double Horizontal (U+2560) - ╠
+		/// </summary>
+		public Rune LeftTeeDbl { get; set; } = (Rune)'╠';
+
+		/// <summary>
+		/// Box Drawings Left Tee - Heavy Horizontal and Light Vertical (U+2523) - ┝
+		/// </summary>
+		public Rune LeftTeeHvH { get; set; } = (Rune)'┝';
+
+		/// <summary>
+		/// Box Drawings Left Tee - Light Horizontal and Heavy Vertical (U+252B) - ┠
+		/// </summary>
+		public Rune LeftTeeHvV { get; set; } = (Rune)'┠';
+
+		/// <summary>
+		/// Box Drawings Left Tee - Heavy Vertical and Heavy Horizontal (U+2527) - ┣
+		/// </summary>
+		public Rune LeftTeeHvDblH { get; set; } = (Rune)'┣';
+
+		/// <summary>
+		/// Box Drawings Righ Tee - Single Vertical and Single Horizontal (U+2524) - ┤
+		/// </summary>
+		public Rune RightTee { get; set; } = (Rune)'┤';
+
+		/// <summary>
+		/// Box Drawings Right Tee - Single Vertical and Double Horizontal (U+2561) - ╡
+		/// </summary>
+		public Rune RightTeeDblH { get; set; } = (Rune)'╡';
+
+		/// <summary>
+		/// Box Drawings Right Tee - Double Vertical and Single Horizontal (U+2562) - ╢
+		/// </summary>
+		public Rune RightTeeDblV { get; set; } = (Rune)'╢';
+
+		/// <summary>
+		/// Box Drawings Right Tee - Double Vertical and Double Horizontal (U+2563) - ╣
+		/// </summary>
+		public Rune RightTeeDbl { get; set; } = (Rune)'╣';
+
+		/// <summary>
+		/// Box Drawings Right Tee - Heavy Horizontal and Light Vertical (U+2528) - ┥
+		/// </summary>
+		public Rune RightTeeHvH { get; set; } = (Rune)'┥';
+
+		/// <summary>
+		/// Box Drawings Right Tee - Light Horizontal and Heavy Vertical (U+2530) - ┨
+		/// </summary>
+		public Rune RightTeeHvV { get; set; } = (Rune)'┨';
+
+		/// <summary>
+		/// Box Drawings Right Tee - Heavy Vertical and Heavy Horizontal (U+252C) - ┫
+		/// </summary>
+		public Rune RightTeeHvDblH { get; set; } = (Rune)'┫';
+
+		/// <summary>
+		/// Box Drawings Top Tee - Single Vertical and Single Horizontal (U+252C) - ┬
+		/// </summary>
+		public Rune TopTee { get; set; } = (Rune)'┬';
+
+		/// <summary>
+		/// Box Drawings Top Tee - Single Vertical and Double Horizontal (U+2564) - ╤
+		/// </summary>
+		public Rune TopTeeDblH { get; set; } = (Rune)'╤';
+
+		/// <summary>
+		/// Box Drawings Top Tee - Double Vertical and Single Horizontal  (U+2565) - ╥
+		/// </summary>
+		public Rune TopTeeDblV { get; set; } = (Rune)'╥';
+
+		/// <summary>
+		/// Box Drawings Top Tee - Double Vertical and Double Horizontal (U+2566) - ╦
+		/// </summary>
+		public Rune TopTeeDbl { get; set; } = (Rune)'╦';
+
+		/// <summary>
+		/// Box Drawings Top Tee - Heavy Horizontal and Light Vertical (U+252F) - ┯
+		/// </summary>
+		public Rune TopTeeHvH { get; set; } = (Rune)'┯';
+
+		/// <summary>
+		/// Box Drawings Top Tee - Light Horizontal and Heavy Vertical (U+2537) - ┰
+		/// </summary>
+		public Rune TopTeeHvV { get; set; } = (Rune)'┰';
+
+		/// <summary>
+		/// Box Drawings Top Tee - Heavy Vertical and Heavy Horizontal (U+2533) - ┳
+		/// </summary>
+		public Rune TopTeeHvDblH { get; set; } = (Rune)'┳';
+
+		/// <summary>
+		/// Box Drawings Bottom Tee - Single Vertical and Single Horizontal (U+2534) - ┴
+		/// </summary>
+		public Rune BottomTee { get; set; } = (Rune)'┴';
+
+		/// <summary>
+		/// Box Drawings Bottom Tee - Single Vertical and Double Horizontal (U+2567) - ╧
+		/// </summary>
+		public Rune BottomTeeDblH { get; set; } = (Rune)'╧';
+
+		/// <summary>
+		/// Box Drawings Bottom Tee - Double Vertical and Single Horizontal (U+2568) - ╨
+		/// </summary>
+		public Rune BottomTeeDblV { get; set; } = (Rune)'╨';
+
+		/// <summary>
+		/// Box Drawings Bottom Tee - Double Vertical and Double Horizontal (U+2569) - ╩
+		/// </summary>
+		public Rune BottomTeeDbl { get; set; } = (Rune)'╩';
+
+		/// <summary>
+		/// Box Drawings Bottom Tee - Heavy Horizontal and Light Vertical (U+2535) - ┷
+		/// </summary>
+		public Rune BottomTeeHvH { get; set; } = (Rune)'┷';
+
+		/// <summary>
+		/// Box Drawings Bottom Tee - Light Horizontal and Heavy Vertical (U+253D) - ┸
+		/// </summary>
+		public Rune BottomTeeHvV { get; set; } = (Rune)'┸';
+
+		/// <summary>
+		/// Box Drawings Bottom Tee - Heavy Vertical and Heavy Horizontal (U+2539) - ┻
+		/// </summary>
+		public Rune BottomTeeHvDblH { get; set; } = (Rune)'┻';
+
+		#endregion
+
+		#region ----------------- Crosses -----------------
+		/// <summary>
+		/// Box Drawings Cross - Single Vertical and Single Horizontal (U+253C) - ┼
+		/// </summary>
+		public Rune Cross { get; set; } = (Rune)'┼';
+
+		/// <summary>
+		/// Box Drawings Cross - Single Vertical and Double Horizontal (U+256A) - ╪
+		/// </summary>
+		public Rune CrossDblH { get; set; } = (Rune)'╪';
+
+		/// <summary>
+		/// Box Drawings Cross - Double Vertical and Single Horizontal (U+256B) - ╫
+		/// </summary>
+		public Rune CrossDblV { get; set; } = (Rune)'╫';
+
+		/// <summary>
+		/// Box Drawings Cross - Double Vertical and Double Horizontal (U+256C) - ╬
+		/// </summary>
+		public Rune CrossDbl { get; set; } = (Rune)'╬';
+
+		/// <summary>
+		/// Box Drawings Cross - Heavy Horizontal and Light Vertical (U+253F) - ┿
+		/// </summary>
+		public Rune CrossHvH { get; set; } = (Rune)'┿';
+
+		/// <summary>
+		/// Box Drawings Cross - Light Horizontal and Heavy Vertical (U+2541) - ╂
+		/// </summary>
+		public Rune CrossHvV { get; set; } = (Rune)'╂';
+
+		/// <summary>
+		/// Box Drawings Cross - Heavy Vertical and Heavy Horizontal (U+254B) - ╋
+		/// </summary>
+		public Rune CrossHv { get; set; } = (Rune)'╋';
+		#endregion
+	}
+}

+ 140 - 90
Terminal.Gui/Drawing/LineCanvas.cs

@@ -3,7 +3,7 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
-using Rune = System.Rune;
+
 
 namespace Terminal.Gui {
 
@@ -16,7 +16,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		None,
 		/// <summary>
-		/// The border is drawn using thin line glyphs.
+		/// The border is drawn using thin line CM.Glyphs.
 		/// </summary>
 		Single,
 		/// <summary>
@@ -28,11 +28,11 @@ namespace Terminal.Gui {
 		/// </summary>
 		Dotted,
 		/// <summary>
-		/// The border is drawn using thin double line glyphs.
+		/// The border is drawn using thin double line CM.Glyphs.
 		/// </summary>
 		Double,
 		/// <summary>
-		/// The border is drawn using heavy line glyphs.
+		/// The border is drawn using heavy line CM.Glyphs.
 		/// </summary>
 		Heavy,
 		/// <summary>
@@ -67,6 +67,21 @@ namespace Terminal.Gui {
 	/// and rendering.  Does not support diagonal lines.
 	/// </summary>
 	public class LineCanvas {
+		/// <summary>
+		/// Creates a new instance.
+		/// </summary>
+		public LineCanvas()
+		{
+			ConfigurationManager.Applied += ConfigurationManager_Applied;
+		}
+
+		private void ConfigurationManager_Applied (object sender, ConfigurationManagerEventArgs e)
+		{
+			foreach (var irr in runeResolvers) {
+				irr.Value.SetGlyphs ();
+			}
+		}
+
 		private List<StraightLine> _lines = new List<StraightLine> ();
 
 		Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> {
@@ -80,7 +95,7 @@ namespace Terminal.Gui {
 			{IntersectionRuneType.RightTee,new RightTeeIntersectionRuneResolver()},
 			{IntersectionRuneType.BottomTee,new BottomTeeIntersectionRuneResolver()},
 
-			{IntersectionRuneType.Crosshair,new CrosshairIntersectionRuneResolver()},
+			{IntersectionRuneType.Cross,new CrossIntersectionRuneResolver()},
 			// TODO: Add other resolvers
 		};
 
@@ -272,26 +287,25 @@ namespace Terminal.Gui {
 		}
 
 		private abstract class IntersectionRuneResolver {
-			readonly Rune round;
-			readonly Rune doubleH;
-			readonly Rune doubleV;
-			readonly Rune doubleBoth;
-			readonly Rune thickH;
-			readonly Rune thickV;
-			readonly Rune thickBoth;
-			readonly Rune normal;
-
-			public IntersectionRuneResolver (Rune round, Rune doubleH, Rune doubleV, Rune doubleBoth, Rune thickH, Rune thickV, Rune thickBoth, Rune normal)
+			internal Rune _round;
+			internal Rune _doubleH;
+			internal Rune _doubleV;
+			internal Rune _doubleBoth;
+			internal Rune _thickH;
+			internal Rune _thickV;
+			internal Rune _thickBoth;
+			internal Rune _normal;
+
+			public IntersectionRuneResolver()
 			{
-				this.round = round;
-				this.doubleH = doubleH;
-				this.doubleV = doubleV;
-				this.doubleBoth = doubleBoth;
-				this.thickH = thickH;
-				this.thickV = thickV;
-				this.thickBoth = thickBoth;
-				this.normal = normal;
+				SetGlyphs ();
 			}
+			
+			/// <summary>
+			/// Sets the glyphs used. Call this method after construction and any time 
+			/// ConfigurationManager has updated the settings.
+			/// </summary>
+			public abstract void SetGlyphs ();
 
 			public Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
 			{
@@ -309,87 +323,140 @@ namespace Terminal.Gui {
 					l.Line.Style == LineStyle.Heavy || l.Line.Style == LineStyle.HeavyDashed || l.Line.Style == LineStyle.HeavyDotted));
 
 				if (doubleHorizontal) {
-					return doubleVertical ? doubleBoth : doubleH;
+					return doubleVertical ? _doubleBoth : _doubleH;
 				}
 				if (doubleVertical) {
-					return doubleV;
+					return _doubleV;
 				}
 
 				if (thickHorizontal) {
-					return thickVertical ? thickBoth : thickH;
+					return thickVertical ? _thickBoth : _thickH;
 				}
 				if (thickVertical) {
-					return thickV;
+					return _thickV;
 				}
 
-				return useRounded ? round : normal;
+				return useRounded ? _round : _normal;
 			}
 		}
 
 		private class ULIntersectionRuneResolver : IntersectionRuneResolver {
-			public ULIntersectionRuneResolver () :
-				base ('╭', '╒', '╓', '╔', '┍', '┎', '┏', '┌')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.ULCornerR;
+				_doubleH = CM.Glyphs.ULCornerSingleDbl;
+				_doubleV = CM.Glyphs.ULCornerDblSingle;
+				_doubleBoth = CM.Glyphs.ULCornerDbl;
+				_thickH = CM.Glyphs.ULCornerLtHv;
+				_thickV = CM.Glyphs.ULCornerHvLt;
+				_thickBoth = CM.Glyphs.ULCornerHv;
+				_normal = CM.Glyphs.ULCorner;
 			}
 		}
 		private class URIntersectionRuneResolver : IntersectionRuneResolver {
-
-			public URIntersectionRuneResolver () :
-				base ('╮', '╕', '╖', '╗', '┑', '┒', '┓', '┐')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.URCornerR;
+				_doubleH = CM.Glyphs.URCornerSingleDbl;
+				_doubleV = CM.Glyphs.URCornerDblSingle;
+				_doubleBoth = CM.Glyphs.URCornerDbl;
+				_thickH = CM.Glyphs.URCornerHvLt;
+				_thickV = CM.Glyphs.URCornerLtHv;
+				_thickBoth = CM.Glyphs.URCornerHv;
+				_normal = CM.Glyphs.URCorner;
 			}
 		}
 		private class LLIntersectionRuneResolver : IntersectionRuneResolver {
-
-			public LLIntersectionRuneResolver () :
-				base ('╰', '╘', '╙', '╚', '┕', '┖', '┗', '└')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.LLCornerR;
+				_doubleH = CM.Glyphs.LLCornerSingleDbl;
+				_doubleV = CM.Glyphs.LLCornerDblSingle;
+				_doubleBoth = CM.Glyphs.LLCornerDbl;
+				_thickH = CM.Glyphs.LLCornerLtHv;
+				_thickV = CM.Glyphs.LLCornerHvLt;
+				_thickBoth = CM.Glyphs.LLCornerHv;
+				_normal = CM.Glyphs.LLCorner;
 			}
+
 		}
 		private class LRIntersectionRuneResolver : IntersectionRuneResolver {
-			public LRIntersectionRuneResolver () :
-				base ('╯', '╛', '╜', '╝', '┙', '┚', '┛', '┘')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.LRCornerR;
+				_doubleH = CM.Glyphs.LRCornerSingleDbl;
+				_doubleV = CM.Glyphs.LRCornerDblSingle;
+				_doubleBoth = CM.Glyphs.LRCornerDbl;
+				_thickH = CM.Glyphs.LRCornerLtHv;
+				_thickV = CM.Glyphs.LRCornerHvLt;
+				_thickBoth = CM.Glyphs.LRCornerHv;
+				_normal = CM.Glyphs.LRCorner;
 			}
 		}
 
 		private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public TopTeeIntersectionRuneResolver () :
-				base ('┬', '╤', '╥', '╦', '┯', '┰', '┳', '┬')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.TopTee;
+				_doubleH = CM.Glyphs.TopTeeDblH;
+				_doubleV = CM.Glyphs.TopTeeDblV;
+				_doubleBoth = CM.Glyphs.TopTeeDbl;
+				_thickH = CM.Glyphs.TopTeeHvH;
+				_thickV = CM.Glyphs.TopTeeHvV;
+				_thickBoth = CM.Glyphs.TopTeeHvDblH;
+				_normal = CM.Glyphs.TopTee;
 			}
 		}
 		private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public LeftTeeIntersectionRuneResolver () :
-				base ('├', '╞', '╟', '╠', '┝', '┠', '┣', '├')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.LeftTee;
+				_doubleH = CM.Glyphs.LeftTeeDblH;
+				_doubleV = CM.Glyphs.LeftTeeDblV;
+				_doubleBoth = CM.Glyphs.LeftTeeDbl;
+				_thickH = CM.Glyphs.LeftTeeHvH;
+				_thickV = CM.Glyphs.LeftTeeHvV;
+				_thickBoth = CM.Glyphs.LeftTeeHvDblH;
+				_normal = CM.Glyphs.LeftTee;
 			}
 		}
 		private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public RightTeeIntersectionRuneResolver () :
-				base ('┤', '╡', '╢', '╣', '┥', '┨', '┫', '┤')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.RightTee;
+				_doubleH = CM.Glyphs.RightTeeDblH;
+				_doubleV = CM.Glyphs.RightTeeDblV;
+				_doubleBoth = CM.Glyphs.RightTeeDbl;
+				_thickH = CM.Glyphs.RightTeeHvH;
+				_thickV = CM.Glyphs.RightTeeHvV;
+				_thickBoth = CM.Glyphs.RightTeeHvDblH;
+				_normal = CM.Glyphs.RightTee;
 			}
 		}
 		private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver {
-			public BottomTeeIntersectionRuneResolver () :
-				base ('┴', '╧', '╨', '╩', '┷', '┸', '┻', '┴')
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.BottomTee;
+				_doubleH = CM.Glyphs.BottomTeeDblH;
+				_doubleV = CM.Glyphs.BottomTeeDblV;
+				_doubleBoth = CM.Glyphs.BottomTeeDbl;
+				_thickH = CM.Glyphs.BottomTeeHvH;
+				_thickV = CM.Glyphs.BottomTeeHvV;
+				_thickBoth = CM.Glyphs.BottomTeeHvDblH;
+				_normal = CM.Glyphs.BottomTee;
 			}
 		}
-		private class CrosshairIntersectionRuneResolver : IntersectionRuneResolver {
-			public CrosshairIntersectionRuneResolver () :
-				base ('┼', '╪', '╫', '╬', '┿', '╂', '╋', '┼')
+		private class CrossIntersectionRuneResolver : IntersectionRuneResolver {
+			public override void SetGlyphs ()
 			{
-
+				_round = CM.Glyphs.Cross;
+				_doubleH = CM.Glyphs.CrossDblH;
+				_doubleV = CM.Glyphs.CrossDblV;
+				_doubleBoth = CM.Glyphs.CrossDbl;
+				_thickH = CM.Glyphs.CrossHvH;
+				_thickV = CM.Glyphs.CrossHvV;
+				_thickBoth = CM.Glyphs.CrossHv;
+				_normal = CM.Glyphs.Cross;
 			}
 		}
 
@@ -421,29 +488,29 @@ namespace Terminal.Gui {
 			case IntersectionRuneType.None:
 				return null;
 			case IntersectionRuneType.Dot:
-				return (Rune)'.';
+				return (Rune)CM.Glyphs.Dot;
 			case IntersectionRuneType.HLine:
 				if (useDouble) {
-					return driver.HDbLine;
+					return CM.Glyphs.HLineDbl;
 				}
 				if (useDashed) {
-					return driver.HDsLine;
+					return CM.Glyphs.HLineDa2;
 				}
 				if (useDotted) {
-					return driver.HDtLine;
+					return CM.Glyphs.HLineDa3;
 				}
-				return useThick ? driver.HThLine : (useThickDashed ? driver.HThDsLine : (useThickDotted ? driver.HThDtLine : driver.HLine));
+				return useThick ? CM.Glyphs.HLineHv : (useThickDashed ? CM.Glyphs.HLineHvDa2 : (useThickDotted ? CM.Glyphs.HLineHvDa3 : CM.Glyphs.HLine));
 			case IntersectionRuneType.VLine:
 				if (useDouble) {
-					return driver.VDbLine;
+					return CM.Glyphs.VLineDbl;
 				}
 				if (useDashed) {
-					return driver.VDsLine;
+					return CM.Glyphs.VLineDa3;
 				}
 				if (useDotted) {
-					return driver.VDtLine;
+					return CM.Glyphs.VLineDa4;
 				}
-				return useThick ? driver.VThLine : (useThickDashed ? driver.VThDsLine : (useThickDotted ? driver.VThDtLine : driver.VLine));
+				return useThick ? CM.Glyphs.VLineHv : (useThickDashed ? CM.Glyphs.VLineHvDa3 : (useThickDotted ? CM.Glyphs.VLineHvDa4 : CM.Glyphs.VLine));
 
 			default: throw new Exception ("Could not find resolver or switch case for " + nameof (runeType) + ":" + runeType);
 			}
@@ -461,23 +528,6 @@ namespace Terminal.Gui {
 
 		}
 
-		/// <summary>
-		/// Represents a single row/column within the <see cref="LineCanvas"/>. Includes the glyph and the foreground/background colors.
-		/// </summary>
-		public class Cell
-		{
-			/// <summary>
-			/// The glyph to draw.
-			/// </summary>
-			public Rune? Rune { get; set; }
-
-			/// <summary>
-			/// The foreground color to draw the glyph with.
-			/// </summary>
-			public Attribute? Attribute { get; set; }
-
-		}
-
 		private Cell GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
 		{
 			if (!intersects.Any ()) {
@@ -494,12 +544,12 @@ namespace Terminal.Gui {
 		{
 			var set = new HashSet<IntersectionType> (intersects.Select (i => i.Type));
 
-			#region Crosshair Conditions
+			#region Cross Conditions
 			if (Has (set,
 				IntersectionType.PassOverHorizontal,
 				IntersectionType.PassOverVertical
 				)) {
-				return IntersectionRuneType.Crosshair;
+				return IntersectionRuneType.Cross;
 			}
 
 			if (Has (set,
@@ -507,7 +557,7 @@ namespace Terminal.Gui {
 				IntersectionType.StartLeft,
 				IntersectionType.StartRight
 				)) {
-				return IntersectionRuneType.Crosshair;
+				return IntersectionRuneType.Cross;
 			}
 
 			if (Has (set,
@@ -515,7 +565,7 @@ namespace Terminal.Gui {
 				IntersectionType.StartUp,
 				IntersectionType.StartDown
 				)) {
-				return IntersectionRuneType.Crosshair;
+				return IntersectionRuneType.Cross;
 			}
 
 			if (Has (set,
@@ -523,7 +573,7 @@ namespace Terminal.Gui {
 				IntersectionType.StartRight,
 				IntersectionType.StartUp,
 				IntersectionType.StartDown)) {
-				return IntersectionRuneType.Crosshair;
+				return IntersectionRuneType.Cross;
 			}
 			#endregion
 
@@ -694,7 +744,7 @@ namespace Terminal.Gui {
 			BottomTee,
 			RightTee,
 			LeftTee,
-			Crosshair,
+			Cross,
 			HLine,
 			VLine,
 		}

+ 2 - 3
Terminal.Gui/Drawing/Ruler.cs

@@ -1,5 +1,4 @@
-using NStack;
-using System;
+using System;
 using System.Collections.Generic;
 using System.Data;
 using System.Text;
@@ -58,7 +57,7 @@ namespace Terminal.Gui {
 				var vrule = _vTemplate.Repeat ((int)Math.Ceiling ((double)(Length + 2) / (double)_vTemplate.Length)) [start..(Length + start)];
 				for (var r = location.Y; r < location.Y + Length; r++) {
 					Application.Driver.Move (location.X, r);
-					Application.Driver.AddRune (vrule [r - location.Y]);
+					Application.Driver.AddRune ((Rune)vrule [r - location.Y]);
 				}
 			}
 		}

+ 21 - 16
Terminal.Gui/Drawing/Thickness.cs

@@ -1,5 +1,4 @@
-using NStack;
-using System;
+using System;
 using System.Collections.Generic;
 using System.Text;
 using System.Text.Json.Serialization;
@@ -82,7 +81,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets the total width of the left and right sides of the rectangle. Sets the height of the left and right sides of the rectangle to half the specified value.
+		/// Gets the total height of the top and bottom sides of the rectangle. Sets the height of the top and bottom sides of the rectangle to half the specified value.
 		/// </summary>
 		public int Vertical {
 			get {
@@ -94,7 +93,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Gets the total width of the top and bottom sides of the rectangle. Sets the width of the top and bottom sides of the rectangle to half the specified value.
+		/// Gets the total width of the left and right sides of the rectangle. Sets the width of the left and rigth sides of the rectangle to half the specified value.
 		/// </summary>
 		public int Horizontal {
 			get {
@@ -143,19 +142,19 @@ namespace Terminal.Gui {
 				return Rect.Empty;
 			}
 
-			System.Rune clearChar = ' ';
-			System.Rune leftChar = clearChar;
-			System.Rune rightChar = clearChar;
-			System.Rune topChar = clearChar;
-			System.Rune bottomChar = clearChar;
+			Rune clearChar = (Rune)' ';
+			Rune leftChar = clearChar;
+			Rune rightChar = clearChar;
+			Rune topChar = clearChar;
+			Rune bottomChar = clearChar;
 
 			if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding) {
-				leftChar = 'L';
-				rightChar = 'R';
-				topChar = 'T';
-				bottomChar = 'B';
+				leftChar = (Rune)'L';
+				rightChar = (Rune)'R';
+				topChar = (Rune)'T';
+				bottomChar = (Rune)'B';
 				if (!string.IsNullOrEmpty (label)) {
-					leftChar = rightChar = bottomChar = topChar = label [0];
+					leftChar = rightChar = bottomChar = topChar = (Rune)label [0];
 				}
 			}
 
@@ -224,7 +223,9 @@ namespace Terminal.Gui {
 		/// </summary>
 		public static Thickness Empty => new Thickness (0);
 
-		/// <inheritdoc/>
+		/// <summary>Determines whether the specified object is equal to the current object.</summary>
+		/// <param name="obj">The object to compare with the current object.</param>
+		/// <returns><c>true</c> if the specified object is equal to the current object; otherwise, <c>false</c>.</returns>
 		public override bool Equals (object obj)
 		{
 			//Check for null and compare run-time types.
@@ -243,7 +244,11 @@ namespace Terminal.Gui {
 		}
 
 		// IEquitable
-		/// <inheritdoc/>
+		/// <summary>
+		/// Indicates whether the current object is equal to another object of the same type.
+		/// </summary>
+		/// <param name="other"></param>
+		/// <returns>true if the current object is equal to the other parameter; otherwise, false.</returns>
 		public bool Equals (Thickness other)
 		{
 			return other is not null &&

+ 13 - 2
Terminal.Gui/FileServices/AllowedType.cs

@@ -33,7 +33,10 @@ namespace Terminal.Gui {
 			return true;
 		}
 
-		/// <inheritdoc/>
+		/// <summary>
+		/// Returns a string representation of this <see cref="AllowedTypeAny"/>.
+		/// </summary>
+		/// <returns></returns>
 		public override string ToString ()
 		{
 			return Strings.fdAnyFiles + "(*.*)";
@@ -91,14 +94,22 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public bool IsAllowed(string path)
 		{
+			if(string.IsNullOrWhiteSpace(path)) {
+				return false;
+			}
+
 			var extension = Path.GetExtension (path);
 
+			if(this.Extensions.Any(e=>path.EndsWith(e, StringComparison.InvariantCultureIgnoreCase))) {
+				return true;
+			}
+
 			// There is a requirement to have a particular extension and we have none
 			if (string.IsNullOrEmpty (extension)) {
 				return false;
 			}
 
-			return this.Extensions.Any (e => e.Equals (extension));
+			return this.Extensions.Any (e => e.Equals (extension, StringComparison.InvariantCultureIgnoreCase));
 		}
 	}
 	

+ 1 - 1
Terminal.Gui/FileServices/DefaultFileOperations.cs

@@ -82,7 +82,7 @@ namespace Terminal.Gui {
 
 			Application.Run (dlg);
 
-			result = tf.Text?.ToString ();
+			result = tf.Text;
 
 			return confirm;
 		}

+ 35 - 0
Terminal.Gui/FileServices/FileDialogIconGetterArgs.cs

@@ -0,0 +1,35 @@
+using System.IO.Abstractions;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Arguments for the <see cref="FileDialogStyle.IconGetter"/> delegate
+	/// </summary>
+	public class FileDialogIconGetterArgs {
+
+		/// <summary>
+		/// Creates a new instance of the class
+		/// </summary>
+		public FileDialogIconGetterArgs (FileDialog fileDialog, IFileSystemInfo file, FileDialogIconGetterContext context)
+		{
+			FileDialog = fileDialog;
+			File = file;
+			Context = context;
+		}
+
+		/// <summary>
+		/// Gets the dialog that requires the icon.
+		/// </summary>
+		public FileDialog FileDialog { get; }
+
+		/// <summary>
+		/// Gets the file/folder for which the icon is required.
+		/// </summary>
+		public IFileSystemInfo File { get; }
+
+		/// <summary>
+		/// Gets the context in which the icon will be used in.
+		/// </summary>
+		public FileDialogIconGetterContext Context { get; }
+	}
+}

+ 18 - 0
Terminal.Gui/FileServices/FileDialogIconGetterContext.cs

@@ -0,0 +1,18 @@
+namespace Terminal.Gui {
+	/// <summary>
+	/// Describes the context in which icons are being sought
+	/// during <see cref="FileDialogIconGetterArgs"/>.
+	/// </summary>
+	public enum FileDialogIconGetterContext {
+
+		/// <summary>
+		/// Icon will be used in the tree view
+		/// </summary>
+		Tree,
+
+		/// <summary>
+		/// Icon will be used in the main table area of the dialog
+		/// </summary>
+		Table
+	}
+}

+ 7 - 3
Terminal.Gui/FileServices/FileDialogRootTreeNode.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.IO;
+using System.IO.Abstractions;
 
 namespace Terminal.Gui {
 
@@ -22,7 +23,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="displayName"></param>
 		/// <param name="path"></param>
-		public FileDialogRootTreeNode (string displayName, DirectoryInfo path)
+		public FileDialogRootTreeNode (string displayName, IDirectoryInfo path)
 		{
 			this.DisplayName = displayName;
 			this.Path = path;
@@ -37,9 +38,12 @@ namespace Terminal.Gui {
 		/// Gets the path that should be shown/explored when selecting this node
 		/// of the tree.
 		/// </summary>
-		public DirectoryInfo Path { get; }
+		public IDirectoryInfo Path { get; }
 
-		/// <inheritdoc/>
+		/// <summary>
+		/// Returns a string representation of this instance (<see cref="DisplayName"/>).
+		/// </summary>
+		/// <returns></returns>
 		public override string ToString ()
 		{
 			return this.DisplayName;

+ 1 - 1
Terminal.Gui/FileServices/FileDialogState.cs

@@ -28,7 +28,7 @@ namespace Terminal.Gui {
 
 		public IDirectoryInfo Directory { get; }
 
-		public FileSystemInfoStats [] Children { get; protected set; }
+		public FileSystemInfoStats [] Children { get; internal set; }
 
 		internal virtual void RefreshChildren ()
 		{

+ 37 - 13
Terminal.Gui/FileServices/FileDialogStyle.cs

@@ -15,6 +15,7 @@ namespace Terminal.Gui {
 	/// Stores style settings for <see cref="FileDialog"/>.
 	/// </summary>
 	public class FileDialogStyle {
+		readonly IFileSystem _fileSystem;
 
 		/// <summary>
 		/// Gets or sets the default value to use for <see cref="UseColors"/>.
@@ -35,12 +36,12 @@ namespace Terminal.Gui {
 		/// should be used for different file types/directories.  Defaults
 		/// to false.
 		/// </summary>
-		public bool UseColors { get; set; }
+		public bool UseColors { get; set; } = DefaultUseColors;
 
 		/// <summary>
 		/// Gets or sets the culture to use (e.g. for number formatting).
 		/// Defaults to <see cref="CultureInfo.CurrentUICulture"/>.
-		/// <summary>
+		/// </summary>
 		public CultureInfo Culture {get;set;} = CultureInfo.CurrentUICulture;
 
 		/// <summary>
@@ -141,7 +142,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets the style settings for the table of files (in currently selected directory).
 		/// </summary>
-		public TableView.TableStyle TableStyle { get; internal set; }
+		public TableStyle TableStyle { get; internal set; }
 
 		/// <summary>
 		/// Gets the style settings for the collapse-able directory/places tree
@@ -155,7 +156,7 @@ namespace Terminal.Gui {
 		/// <see cref="Environment.SpecialFolder"/>.
 		/// </summary>
 		/// <remarks>Must be configured before showing the dialog.</remarks>
-		public FileDialogTreeRootGetter TreeRootGetter { get; set; } = DefaultTreeRootGetter;
+		public FileDialogTreeRootGetter TreeRootGetter { get; set; }
 
 		/// <summary>
 		/// Gets or sets whether to use advanced unicode characters which might not be installed
@@ -167,7 +168,7 @@ namespace Terminal.Gui {
 		/// User defined delegate for picking which character(s)/unicode
 		/// symbol(s) to use as an 'icon' for files/folders. 
 		/// </summary>
-		public Func<IFileSystemInfo, string> IconGetter { get; set; }
+		public Func<FileDialogIconGetterArgs, string> IconGetter { get; set; }
 
 		/// <summary>
 		/// Gets or sets the format to use for date/times in the Modified column.
@@ -179,9 +180,17 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Creates a new instance of the <see cref="FileDialogStyle"/> class.
 		/// </summary>
-		public FileDialogStyle ()
+		public FileDialogStyle (IFileSystem fileSystem)
 		{
+			_fileSystem = fileSystem;
 			IconGetter = DefaultIconGetter;
+			TreeRootGetter = DefaultTreeRootGetter;
+
+			if(NerdFonts.Enable)
+			{
+				UseNerdForIcons();
+			}
+
 			DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern;
 
 			ColorSchemeDirectory = new ColorScheme {
@@ -210,25 +219,39 @@ namespace Terminal.Gui {
 				Focus = Application.Driver.MakeAttribute (Color.Black, Color.White),
 				HotFocus = Application.Driver.MakeAttribute (Color.Black, Color.White),
 			};
+		}
 
+		/// <summary>
+		/// Changes <see cref="IconGetter"/> to serve diverse icon set using
+		/// the Nerd fonts. This option requires users to have specific font(s)
+		/// installed.
+		/// </summary>
+		public void UseNerdForIcons ()
+		{
+			var nerd = new NerdFonts();
+			IconGetter = nerd.GetNerdIcon;
 		}
 
-		private string DefaultIconGetter (IFileSystemInfo arg)
+		private string DefaultIconGetter (FileDialogIconGetterArgs args)
 		{
-			if (arg is IDirectoryInfo) {
-				return UseUnicodeCharacters ? "\ua909 " : "\\";
+			var file = args.File;
+
+			if (file is IDirectoryInfo) {
+				return UseUnicodeCharacters ? ConfigurationManager.Glyphs.Folder + " " : Path.DirectorySeparatorChar.ToString();
 			}
 
-			return UseUnicodeCharacters ? "\u2630 " : "";
+			return UseUnicodeCharacters ?  ConfigurationManager.Glyphs.File + " " : "";
 
 		}
 
-		private static IEnumerable<FileDialogRootTreeNode> DefaultTreeRootGetter ()
+		private IEnumerable<FileDialogRootTreeNode> DefaultTreeRootGetter ()
 		{
 			var roots = new List<FileDialogRootTreeNode> ();
 			try {
 				foreach (var d in Environment.GetLogicalDrives ()) {
-					roots.Add (new FileDialogRootTreeNode (d, new DirectoryInfo (d)));
+
+					
+					roots.Add (new FileDialogRootTreeNode (d, _fileSystem.DirectoryInfo.New(d)));
 				}
 
 			} catch (Exception) {
@@ -246,7 +269,8 @@ namespace Terminal.Gui {
 
 							roots.Add (new FileDialogRootTreeNode (
 							special.ToString (),
-							new DirectoryInfo (Environment.GetFolderPath (special))));
+							_fileSystem.DirectoryInfo.New(Environment.GetFolderPath (special))
+							));
 						}
 					} catch (Exception) {
 						// Special file exists but contents are unreadable (permissions?)

+ 34 - 4
Terminal.Gui/FileServices/FileDialogTreeBuilder.cs

@@ -1,11 +1,19 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.IO.Abstractions;
 using System.Linq;
 
 namespace Terminal.Gui {
 
 	class FileDialogTreeBuilder : ITreeBuilder<object> {
+		readonly FileDialog _dlg;
+
+		public FileDialogTreeBuilder(FileDialog dlg)
+		{
+			_dlg = dlg;
+		}
+
 		public bool SupportsCanExpand => true;
 
 		public bool CanExpand (object toExpand)
@@ -18,18 +26,40 @@ namespace Terminal.Gui {
 			return this.TryGetDirectories (NodeToDirectory (forObject));
 		}
 
-		internal static DirectoryInfo NodeToDirectory (object toExpand)
+		internal static IDirectoryInfo NodeToDirectory (object toExpand)
 		{
-			return toExpand is FileDialogRootTreeNode f ? f.Path : (DirectoryInfo)toExpand;
+			return toExpand is FileDialogRootTreeNode f ? f.Path : (IDirectoryInfo)toExpand;
+		}
+
+		internal string AspectGetter(object o)
+		{
+			string icon;
+			string name;
+
+			if(o is FileDialogRootTreeNode r)
+			{
+				icon = _dlg.Style.IconGetter.Invoke(
+					new FileDialogIconGetterArgs(_dlg, r.Path, FileDialogIconGetterContext.Tree));
+				name = r.DisplayName;
+			}
+			else
+			{
+				var dir  = (IDirectoryInfo)o;
+				icon = _dlg.Style.IconGetter.Invoke(
+					new FileDialogIconGetterArgs(_dlg, dir, FileDialogIconGetterContext.Tree));
+				name = dir.Name;
+			}
+
+			return icon + name;
 		}
 
-		private IEnumerable<DirectoryInfo> TryGetDirectories (DirectoryInfo directoryInfo)
+		private IEnumerable<IDirectoryInfo> TryGetDirectories (IDirectoryInfo directoryInfo)
 		{
 			try {
 				return directoryInfo.EnumerateDirectories ();
 			} catch (Exception) {
 
-				return Enumerable.Empty<DirectoryInfo> ();
+				return Enumerable.Empty<IDirectoryInfo> ();
 			}
 		}
 

+ 2 - 28
Terminal.Gui/FileServices/FileSystemInfoStats.cs

@@ -73,7 +73,7 @@ namespace Terminal.Gui {
 
 		public bool IsImage ()
 		{
-			return this.FileSystemInfo is FileSystemInfo f &&
+			return this.FileSystemInfo is IFileSystemInfo f &&
 				ImageExtensions.Contains (
 					f.Extension,
 					StringComparer.InvariantCultureIgnoreCase);
@@ -82,38 +82,12 @@ namespace Terminal.Gui {
 		public bool IsExecutable ()
 		{
 			// TODO: handle linux executable status
-			return this.FileSystemInfo is FileSystemInfo f &&
+			return this.FileSystemInfo is IFileSystemInfo f &&
 				ExecutableExtensions.Contains (
 					f.Extension,
 					StringComparer.InvariantCultureIgnoreCase);
 		}
 
-		internal object GetOrderByValue (FileDialog dlg, string columnName)
-		{
-			if (dlg.Style.FilenameColumnName == columnName)
-				return this.FileSystemInfo.Name;
-
-			if (dlg.Style.SizeColumnName == columnName)
-				return this.MachineReadableLength;
-
-			if (dlg.Style.ModifiedColumnName == columnName)
-				return this.LastWriteTime;
-
-			if (dlg.Style.TypeColumnName == columnName)
-				return this.Type;
-
-			throw new ArgumentOutOfRangeException ("Unknown column " + nameof (columnName));
-		}
-
-		internal object GetOrderByDefault ()
-		{
-			if (this.IsDir ()) {
-				return -1;
-			}
-
-			return 100;
-		}
-
 		private static string GetHumanReadableFileSize (long value, CultureInfo culture)
 		{
 

+ 1 - 1
Terminal.Gui/Input/KeystrokeNavigatorEventArgs.cs

@@ -2,7 +2,7 @@
 
 namespace Terminal.Gui {
 	/// <summary>
-	/// Event arguments for the <see cref="CollectionNavigator.SearchStringChanged"/> event.
+	/// Event arguments for the <see cref="CollectionNavigatorBase.SearchStringChanged"/> event.
 	/// </summary>
 	public class KeystrokeNavigatorEventArgs : EventArgs {
 		/// <summary>

+ 13 - 14
Terminal.Gui/Input/ShortcutHelper.cs

@@ -1,5 +1,4 @@
-using NStack;
-using System;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
@@ -27,7 +26,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The keystroke combination used in the <see cref="Shortcut"/> as string.
 		/// </summary>
-		public virtual ustring ShortcutTag => GetShortcutTag (shortcut);
+		public virtual string ShortcutTag => GetShortcutTag (shortcut);
 
 		/// <summary>
 		/// The action to run if the <see cref="Shortcut"/> is defined.
@@ -61,7 +60,7 @@ namespace Terminal.Gui {
 		/// <param name="shortcut">The shortcut key.</param>
 		/// <param name="delimiter">The delimiter string.</param>
 		/// <returns></returns>
-		public static ustring GetShortcutTag (Key shortcut, ustring delimiter = null)
+		public static string GetShortcutTag (Key shortcut, string delimiter = null)
 		{
 			if (shortcut == Key.Null) {
 				return "";
@@ -71,7 +70,7 @@ namespace Terminal.Gui {
 			if (delimiter == null) {
 				delimiter = MenuBar.ShortcutDelimiter;
 			}
-			ustring tag = ustring.Empty;
+			string tag = string.Empty;
 			var sCut = GetKeyToString (k, out Key knm).ToString ();
 			if (knm == Key.Unknown) {
 				k &= ~Key.Unknown;
@@ -81,25 +80,25 @@ namespace Terminal.Gui {
 				tag = "Ctrl";
 			}
 			if ((k & Key.ShiftMask) != 0) {
-				if (!tag.IsEmpty) {
+				if (!string.IsNullOrEmpty(tag)) {
 					tag += delimiter;
 				}
 				tag += "Shift";
 			}
 			if ((k & Key.AltMask) != 0) {
-				if (!tag.IsEmpty) {
+				if (!string.IsNullOrEmpty(tag)) {
 					tag += delimiter;
 				}
 				tag += "Alt";
 			}
 
-			ustring [] keys = ustring.Make (sCut).Split (",");
+			string [] keys = sCut.Split (",");
 			for (int i = 0; i < keys.Length; i++) {
-				var key = keys [i].TrimSpace ();
+				var key = keys [i].Trim ();
 				if (key == Key.AltMask.ToString () || key == Key.ShiftMask.ToString () || key == Key.CtrlMask.ToString ()) {
 					continue;
 				}
-				if (!tag.IsEmpty) {
+				if (!string.IsNullOrEmpty(tag)) {
 					tag += delimiter;
 				}
 				if (!key.Contains ("F") && key.Length > 2 && keys.Length == 1) {
@@ -120,7 +119,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="key">The key to extract.</param>
 		/// <param name="knm">Correspond to the non modifier key.</param>
-		public static ustring GetKeyToString (Key key, out Key knm)
+		public static string GetKeyToString (Key key, out Key knm)
 		{
 			if (key == Key.Null) {
 				knm = Key.Null;
@@ -150,10 +149,10 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="tag">The key as string.</param>
 		/// <param name="delimiter">The delimiter string.</param>
-		public static Key GetShortcutFromTag (ustring tag, ustring delimiter = null)
+		public static Key GetShortcutFromTag (string tag, string delimiter = null)
 		{
 			var sCut = tag;
-			if (sCut.IsEmpty) {
+			if (string.IsNullOrEmpty(sCut)) {
 				return default;
 			}
 
@@ -163,7 +162,7 @@ namespace Terminal.Gui {
 				delimiter = MenuBar.ShortcutDelimiter;
 			}
 
-			ustring [] keys = sCut.Split (delimiter);
+			string [] keys = sCut.Split (delimiter);
 			for (int i = 0; i < keys.Length; i++) {
 				var k = keys [i];
 				if (k == "Ctrl") {

+ 0 - 13
Terminal.Gui/MainLoop.cs

@@ -46,19 +46,6 @@ namespace Terminal.Gui {
 	///   does not seem to be a way of supporting this on Windows.
 	/// </remarks>
 	public class MainLoop {
-		/// <summary>
-		/// Provides data for timers running manipulation.
-		/// </summary>
-		public sealed class Timeout {
-			/// <summary>
-			/// Time to wait before invoke the callback.
-			/// </summary>
-			public TimeSpan Span;
-			/// <summary>
-			/// The function that will be invoked.
-			/// </summary>
-			public Func<MainLoop, bool> Callback;
-		}
 
 		internal SortedList<long, Timeout> timeouts = new SortedList<long, Timeout> ();
 		object _timeoutsLockToken = new object ();

+ 9 - 5
Terminal.Gui/Resources/config.json

@@ -1,16 +1,21 @@
 {
-  // This document specifies the "source of truth" for default values for all Terminal.GUi settings managed by
+  // Specifies the "source of truth" for default values for all Terminal.Gui settings managed by
   // ConfigurationManager. It is automatically loaded, and applied, each time Application.Init
   // is run (via the ConfiguraitonManager.Reset method). 
   //
   // In otherwords, initial values set in the the codebase are always overwritten by the contents of this 
-  // file.
+  // resource embedded in the Terminal.Gui.dll assembly.
   //
-  // The Unit Test method "TestConfigurationManagerSaveDefaults" can be used to re-create the base of this file, but
+  // The Unit Test method "ConfigurationManagerTests.SaveDefaults" can be used to re-create the base of this file, but
   // note that not all values here will be recreated (e.g. the Light and Dark themes and any property initialized
-  // null.
+  // null).
   //
   "$schema": "https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json",
+
+  // Set this to true in a .config file to be loaded to cause JSON parsing errors
+  // to throw exceptions. 
+  "ConfigurationManager.ThrowOnJsonErrors": false,
+
   "Application.AlternateBackwardKey": {
     "Key": "PageUp",
     "Modifiers": [
@@ -30,7 +35,6 @@
       "Ctrl"
     ]
   },
-  "Application.UseSystemConsole": false,
   "Application.IsMouseDisabled": false,
   "Theme": "Default",
   "Themes": [

+ 79 - 0
Terminal.Gui/RunState.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// The execution state for a <see cref="Toplevel"/> view.
+/// </summary>
+public class RunState : IDisposable {
+	/// <summary>
+	/// Initializes a new <see cref="RunState"/> class.
+	/// </summary>
+	/// <param name="view"></param>
+	public RunState (Toplevel view)
+	{
+		Toplevel = view;
+	}
+	/// <summary>
+	/// The <see cref="Toplevel"/> belonging to this <see cref="RunState"/>.
+	/// </summary>
+	public Toplevel Toplevel { get; internal set; }
+
+#if DEBUG_IDISPOSABLE
+	/// <summary>
+	/// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
+	/// </summary>
+	public bool WasDisposed = false;
+
+	/// <summary>
+	/// For debug (see DEBUG_IDISPOSABLE define) purposes to verify objects are being disposed properly
+	/// </summary>
+	public int DisposedCount = 0;
+
+	/// <summary>
+	/// For debug (see DEBUG_IDISPOSABLE define) purposes; the runstate instances that have been created
+	/// </summary>
+	public static List<RunState> Instances = new List<RunState> ();
+
+	/// <summary>
+	/// Creates a new RunState object.
+	/// </summary>
+	public RunState ()
+	{
+		Instances.Add (this);
+	}
+#endif
+
+	/// <summary>
+	/// Releases all resource used by the <see cref="RunState"/> object.
+	/// </summary>
+	/// <remarks>
+	/// Call <see cref="Dispose()"/> when you are finished using the <see cref="RunState"/>. 
+	/// </remarks>
+	/// <remarks>
+	/// <see cref="Dispose()"/> method leaves the <see cref="RunState"/> in an unusable state. After
+	/// calling <see cref="Dispose()"/>, you must release all references to the
+	/// <see cref="RunState"/> so the garbage collector can reclaim the memory that the
+	/// <see cref="RunState"/> was occupying.
+	/// </remarks>
+	public void Dispose ()
+	{
+		Dispose (true);
+		GC.SuppressFinalize (this);
+#if DEBUG_IDISPOSABLE
+		WasDisposed = true;
+#endif
+	}
+
+	/// <summary>
+	/// Releases all resource used by the <see cref="RunState"/> object.
+	/// </summary>
+	/// <param name="disposing">If set to <see langword="true"/> we are disposing and should dispose held objects.</param>
+	protected virtual void Dispose (bool disposing)
+	{
+		if (Toplevel != null && disposing) {
+			throw new InvalidOperationException ("You must clean up (Dispose) the Toplevel before calling Application.RunState.Dispose");
+		}
+	}
+}

+ 0 - 32
Terminal.Gui/StringExtensions.cs

@@ -1,32 +0,0 @@
-using System.Text;
-
-namespace Terminal.Gui {
-	/// <summary>
-	/// Extension helper of <see cref="System.String"/> to work with specific text manipulation./>
-	/// </summary>
-	public static class StringExtensions {
-		/// <summary>
-		/// Repeats the <paramref name="instr"/> <paramref name="n"/>  times.
-		/// </summary>
-		/// <param name="instr">The text to repeat.</param>
-		/// <param name="n">Number of times to repeat the text.</param>
-		/// <returns>
-		///  The text repeated if <paramref name="n"/> is greater than zero, 
-		///  otherwise <see langword="null"/>.
-		/// </returns>
-		public static string Repeat (this string instr, int n)
-		{
-			if (n <= 0) {
-				return null;
-			}
-
-			if (string.IsNullOrEmpty (instr) || n == 1) {
-				return instr;
-			}
-
-			return new StringBuilder (instr.Length * n)
-				.Insert (0, instr, n)
-				.ToString ();
-		}
-	}
-}

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

@@ -5,6 +5,7 @@
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
     <DebugType>portable</DebugType>
+	<WarningsAsErrors>CS1574<!--,CA1034--></WarningsAsErrors>
   </PropertyGroup>
   <PropertyGroup>
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
@@ -26,7 +27,6 @@
     <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
     <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
-    <PackageReference Include="NStack.Core" Version="1.0.7" />
     <PackageReference Include="System.IO.Abstractions" Version="19.2.4" />
     <PackageReference Include="System.Text.Json" Version="7.0.1" />
     <PackageReference Include="System.Management" Version="7.0.0" />
@@ -63,7 +63,7 @@
   </ItemGroup>
   <PropertyGroup>
     <TargetFrameworks>net7.0</TargetFrameworks>
-    <LangVersion>9.0</LangVersion>
+    <LangVersion>10.0</LangVersion>
     <RootNamespace>Terminal.Gui</RootNamespace>
     <AssemblyName>Terminal.Gui</AssemblyName>
     <DocumentationFile>bin\Release\Terminal.Gui.xml</DocumentationFile>

+ 19 - 23
Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs

@@ -1,6 +1,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using System.Text;
 
 namespace Terminal.Gui {
 
@@ -30,12 +31,12 @@ namespace Terminal.Gui {
 			this.textField = textField;
 			SelectionKey = Key.Tab;
 
-			ColorScheme = new ColorScheme{
-				Normal = new Attribute(Color.DarkGray,0),
-				Focus = new Attribute(Color.DarkGray,0),
-				HotNormal = new Attribute(Color.DarkGray,0),
-				HotFocus = new Attribute(Color.DarkGray,0),
-				Disabled = new Attribute(Color.DarkGray,0),
+			ColorScheme = new ColorScheme {
+				Normal = new Attribute (Color.DarkGray, 0),
+				Focus = new Attribute (Color.DarkGray, 0),
+				HotNormal = new Attribute (Color.DarkGray, 0),
+				HotFocus = new Attribute (Color.DarkGray, 0),
+				Disabled = new Attribute (Color.DarkGray, 0),
 			};
 		}
 
@@ -64,16 +65,13 @@ namespace Terminal.Gui {
 			} else
 			if (key == Key.CursorDown) {
 				return this.CycleSuggestion (-1);
-			}
-			else if(key == CloseKey && Suggestions.Any())
-			{
-				ClearSuggestions();
+			} else if (key == CloseKey && Suggestions.Any ()) {
+				ClearSuggestions ();
 				_suspendSuggestions = true;
 				return true;
 			}
 
-			if(char.IsLetterOrDigit((char)kb.KeyValue))
-			{
+			if (char.IsLetterOrDigit ((char)kb.KeyValue)) {
 				_suspendSuggestions = false;
 			}
 
@@ -84,8 +82,7 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override void GenerateSuggestions (AutocompleteContext context)
 		{
-			if(_suspendSuggestions)
-			{
+			if (_suspendSuggestions) {
 				return;
 			}
 			base.GenerateSuggestions (context);
@@ -107,14 +104,13 @@ namespace Terminal.Gui {
 			var suggestion = this.Suggestions.ElementAt (this.SelectedIdx);
 			var fragment = suggestion.Replacement.Substring (suggestion.Remove);
 
-			int spaceAvailable = textField.Bounds.Width - textField.Text.ConsoleWidth;
-			int spaceRequired = fragment.Sum(c=>Rune.ColumnWidth(c));
+			int spaceAvailable = textField.Bounds.Width - textField.Text.GetColumns ();
+			int spaceRequired = fragment.EnumerateRunes ().Sum (c => c.GetColumns ());
 
-			if(spaceAvailable < spaceRequired)
-			{
-				fragment = new string(
-					fragment.TakeWhile(c=> (spaceAvailable -= Rune.ColumnWidth(c)) >= 0)
-					.ToArray()
+			if (spaceAvailable < spaceRequired) {
+				fragment = new string (
+					fragment.TakeWhile (c => (spaceAvailable -= ((Rune)c).GetColumns ()) >= 0)
+					.ToArray ()
 				);
 			}
 
@@ -132,12 +128,12 @@ namespace Terminal.Gui {
 			if (this.MakingSuggestion ()) {
 
 				var insert = this.Suggestions.ElementAt (this.SelectedIdx);
-				var newText = textField.Text.ToString ();
+				var newText = textField.Text;
 				newText = newText.Substring (0, newText.Length - insert.Remove);
 				newText += insert.Replacement;
 				textField.Text = newText;
 
-				this.textField.MoveEnd();
+				this.textField.MoveEnd ();
 
 				this.ClearSuggestions ();
 				return true;

+ 1 - 1
Terminal.Gui/Text/Autocomplete/AutocompleteContext.cs

@@ -1,5 +1,5 @@
 using System.Collections.Generic;
-using Rune = System.Rune;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>

+ 1 - 1
Terminal.Gui/Text/Autocomplete/IAutocomplete.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
-using Rune = System.Rune;
+
 
 namespace Terminal.Gui {
 

+ 1 - 1
Terminal.Gui/Text/Autocomplete/ISuggestionGenerator.cs

@@ -1,5 +1,5 @@
 using System.Collections.Generic;
-using Rune = System.Rune;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>

+ 4 - 3
Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
 using System.Text;
-using Rune = System.Rune;
+
 
 namespace Terminal.Gui {
 
@@ -34,7 +34,7 @@ namespace Terminal.Gui {
 				}
 			}
 
-			public override void Redraw (Rect bounds)
+			public override void OnDrawContent (Rect contentArea)
 			{
 				if (autocomplete.LastPopupPos == null) {
 					return;
@@ -43,6 +43,7 @@ namespace Terminal.Gui {
 				autocomplete.RenderOverlay ((Point)autocomplete.LastPopupPos);
 			}
 
+
 			public override bool MouseEvent (MouseEvent mouseEvent)
 			{
 				return autocomplete.MouseEvent (mouseEvent);
@@ -274,7 +275,7 @@ namespace Terminal.Gui {
 		/// <returns><c>true</c>if the key can be handled <c>false</c>otherwise.</returns>
 		public override bool ProcessKey (KeyEvent kb)
 		{
-			if (SuggestionGenerator.IsWordChar ((char)kb.Key)) {
+			if (SuggestionGenerator.IsWordChar ((Rune)(char)kb.Key)) {
 				Visible = true;
 				ManipulatePopup ();
 				closed = false;

+ 3 - 3
Terminal.Gui/Text/Autocomplete/SingleWordSuggestionGenerator.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
-using Rune = System.Rune;
+
 
 namespace Terminal.Gui {
 	
@@ -50,7 +50,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public virtual bool IsWordChar (Rune rune)
 		{
-			return Char.IsLetterOrDigit ((char)rune);
+			return Char.IsLetterOrDigit ((char)rune.Value);
 		}
 
 		/// <summary>
@@ -92,7 +92,7 @@ namespace Terminal.Gui {
 			// we are at the end of a word. Work out what has been typed so far
 			while (endIdx-- > 0) {
 				if (IsWordChar (line [endIdx])) {
-					sb.Insert (0, (char)line [endIdx]);
+					sb.Insert (0, (char)line [endIdx].Value);
 				} else {
 					break;
 				}

+ 16 - 201
Terminal.Gui/Text/CollectionNavigator.cs

@@ -1,225 +1,40 @@
 using System;
+using System.Collections;
 using System.Collections.Generic;
 using System.Linq;
 
 namespace Terminal.Gui {
-	/// <summary>
-	/// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. 
-	/// The <see cref="SearchString"/> is used to find the next item in the collection that matches the search string
-	/// when <see cref="GetNextMatchingItem(int, char)"/> is called.
-	/// <para>
-	/// If the user types keystrokes that can't be found in the collection, 
-	/// the search string is cleared and the next item is found that starts with the last keystroke.
-	/// </para>
-	/// <para>
-	/// If the user pauses keystrokes for a short time (see <see cref="TypingDelay"/>), the search string is cleared.
-	/// </para>
-	/// </summary>
-	public partial class CollectionNavigator {
-		/// <summary>
-		/// Constructs a new CollectionNavigator.
-		/// </summary>
-		public CollectionNavigator () { }
-
-		/// <summary>
-		/// Constructs a new CollectionNavigator for the given collection.
-		/// </summary>
-		/// <param name="collection"></param>
-		public CollectionNavigator (IEnumerable<object> collection) => Collection = collection;
-
-		DateTime lastKeystroke = DateTime.Now;
-		/// <summary>
-		/// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is
-		/// reset on each call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
-		/// </summary>
-		public int TypingDelay { get; set; } = 500;
-
-		/// <summary>
-		/// The compararer function to use when searching the collection.
-		/// </summary>
-		public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
 
+	/// <inheritdoc/>
+	/// <remarks>This implementation is based on a static <see cref="Collection"/> of objects.</remarks>
+	public class CollectionNavigator : CollectionNavigatorBase {
 		/// <summary>
 		/// The collection of objects to search. <see cref="object.ToString()"/> is used to search the collection.
 		/// </summary>
-		public IEnumerable<object> Collection { get; set; }
-
-		/// <summary>
-		/// This event is invoked when <see cref="SearchString"/>  changes. Useful for debugging.
-		/// </summary>
-		public event EventHandler<KeystrokeNavigatorEventArgs> SearchStringChanged;
-
-		private string _searchString = "";
-		/// <summary>
-		/// Gets the current search string. This includes the set of keystrokes that have been pressed
-		/// since the last unsuccessful match or after <see cref="TypingDelay"/>) milliseconds. Useful for debugging.
-		/// </summary>
-		public string SearchString {
-			get => _searchString;
-			private set {
-				_searchString = value;
-				OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value));
-			}
-		}
+		public IList Collection { get; set; }
 
 		/// <summary>
-		/// Invoked when the <see cref="SearchString"/> changes. Useful for debugging. Invokes the <see cref="SearchStringChanged"/> event.
-		/// </summary>
-		/// <param name="e"></param>
-		public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e)
-		{
-			SearchStringChanged?.Invoke (this, e);
-		}
-
-		/// <summary>
-		/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> plus the provided character (typically
-		/// from a key press).
+		/// Constructs a new CollectionNavigator.
 		/// </summary>
-		/// <param name="currentIndex">The index in the collection to start the search from.</param>
-		/// <param name="keyStruck">The character of the key the user pressed.</param>
-		/// <returns>The index of the item that matches what the user has typed. 
-		/// Returns <see langword="-1"/> if no item in the collection matched.</returns>
-		public int GetNextMatchingItem (int currentIndex, char keyStruck)
-		{
-			AssertCollectionIsNotNull ();
-			if (!char.IsControl (keyStruck)) {
-
-				// maybe user pressed 'd' and now presses 'd' again.
-				// a candidate search is things that begin with "dd"
-				// but if we find none then we must fallback on cycling
-				// d instead and discard the candidate state
-				string candidateState = "";
-
-				// is it a second or third (etc) keystroke within a short time
-				if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
-					// "dd" is a candidate
-					candidateState = SearchString + keyStruck;
-				} else {
-					// its a fresh keystroke after some time
-					// or its first ever key press
-					SearchString = new string (keyStruck, 1);
-				}
-
-				var idxCandidate = GetNextMatchingItem (currentIndex, candidateState,
-					// prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
-					candidateState.Length > 1);
-
-				if (idxCandidate != -1) {
-					// found "dd" so candidate searchstring is accepted
-					lastKeystroke = DateTime.Now;
-					SearchString = candidateState;
-					return idxCandidate;
-				}
-
-				//// nothing matches "dd" so discard it as a candidate
-				//// and just cycle "d" instead
-				lastKeystroke = DateTime.Now;
-				idxCandidate = GetNextMatchingItem (currentIndex, candidateState);
-
-				// if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z'
-				// instead of "can" + 'd').
-				if (SearchString.Length > 1 && idxCandidate == -1) {
-					// ignore it since we're still within the typing delay
-					// don't add it to SearchString either
-					return currentIndex;
-				}
-
-				// if no changes to current state manifested
-				if (idxCandidate == currentIndex || idxCandidate == -1) {
-					// clear history and treat as a fresh letter
-					ClearSearchString ();
-					
-					// match on the fresh letter alone
-					SearchString = new string (keyStruck, 1);
-					idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
-					return idxCandidate == -1 ? currentIndex : idxCandidate;
-				}
-
-				// Found another "d" or just leave index as it was
-				return idxCandidate;
-
-			} else {
-				// clear state because keypress was a control char
-				ClearSearchString ();
-
-				// control char indicates no selection
-				return -1;
-			}
-		}
+		public CollectionNavigator () { }
 
 		/// <summary>
-		/// Gets the index of the next item in the collection that matches <paramref name="search"/>. 
+		/// Constructs a new CollectionNavigator for the given collection.
 		/// </summary>
-		/// <param name="currentIndex">The index in the collection to start the search from.</param>
-		/// <param name="search">The search string to use.</param>
-		/// <param name="minimizeMovement">Set to <see langword="true"/> to stop the search on the first match
-		/// if there are multiple matches for <paramref name="search"/>.
-		/// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If <see langword="false"/> (the default), 
-		/// the next matching item will be returned, even if it is above in the collection.
-		/// </param>
-		/// <returns>The index of the next matching item or <see langword="-1"/> if no match was found.</returns>
-		internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false)
-		{
-			if (string.IsNullOrEmpty (search)) {
-				return -1;
-			}
-			AssertCollectionIsNotNull ();
-
-			// find indexes of items that start with the search text
-			int [] matchingIndexes = Collection.Select ((item, idx) => (item, idx))
-				  .Where (k => k.item?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false)
-				  .Select (k => k.idx)
-				  .ToArray ();
-
-			// if there are items beginning with search
-			if (matchingIndexes.Length > 0) {
-				// is one of them currently selected?
-				var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex);
-
-				if (currentlySelected == -1) {
-					// we are not currently selecting any item beginning with the search
-					// so jump to first item in list that begins with the letter
-					return matchingIndexes [0];
-				} else {
-
-					// the current index is part of the matching collection
-					if (minimizeMovement) {
-						// if we would rather not jump around (e.g. user is typing lots of text to get this match)
-						return matchingIndexes [currentlySelected];
-					}
-
-					// cycle to next (circular)
-					return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length];
-				}
-			}
-
-			// nothing starts with the search
-			return -1;
-		}
+		/// <param name="collection"></param>
+		public CollectionNavigator (IList collection) => Collection = collection;
 
-		private void AssertCollectionIsNotNull ()
+		/// <inheritdoc/>
+		protected override object ElementAt (int idx)
 		{
-			if (Collection == null) {
-				throw new InvalidOperationException ("Collection is null");
-			}
+			return Collection [idx];
 		}
 
-		private void ClearSearchString ()
+		/// <inheritdoc/>
+		protected override int GetCollectionLength ()
 		{
-			SearchString = "";
-			lastKeystroke = DateTime.Now;
+			return Collection.Count;
 		}
 
-		/// <summary>
-		/// Returns true if <paramref name="kb"/> is a searchable key
-		/// (e.g. letters, numbers, etc) that are valid to pass to this
-		/// class for search filtering.
-		/// </summary>
-		/// <param name="kb"></param>
-		/// <returns></returns>
-		public static bool IsCompatibleKey (KeyEvent kb)
-		{
-			return !kb.IsAlt && !kb.IsCtrl;
-		}
 	}
 }

+ 217 - 0
Terminal.Gui/Text/CollectionNavigatorBase.cs

@@ -0,0 +1,217 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. 
+	/// The <see cref="SearchString"/> is used to find the next item in the collection that matches the search string
+	/// when <see cref="GetNextMatchingItem(int, char)"/> is called.
+	/// <para>
+	/// If the user types keystrokes that can't be found in the collection, 
+	/// the search string is cleared and the next item is found that starts with the last keystroke.
+	/// </para>
+	/// <para>
+	/// If the user pauses keystrokes for a short time (see <see cref="TypingDelay"/>), the search string is cleared.
+	/// </para>
+	/// </summary>
+	public abstract class CollectionNavigatorBase {
+
+		DateTime lastKeystroke = DateTime.Now;
+		/// <summary>
+		/// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is
+		/// reset on each call to <see cref="GetNextMatchingItem(int, char)"/>. The default is 500ms.
+		/// </summary>
+		public int TypingDelay { get; set; } = 500;
+
+		/// <summary>
+		/// The compararer function to use when searching the collection.
+		/// </summary>
+		public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
+
+		/// <summary>
+		/// This event is invoked when <see cref="SearchString"/>  changes. Useful for debugging.
+		/// </summary>
+		public event EventHandler<KeystrokeNavigatorEventArgs> SearchStringChanged;
+
+		private string _searchString = "";
+		/// <summary>
+		/// Gets the current search string. This includes the set of keystrokes that have been pressed
+		/// since the last unsuccessful match or after <see cref="TypingDelay"/>) milliseconds. Useful for debugging.
+		/// </summary>
+		public string SearchString {
+			get => _searchString;
+			private set {
+				_searchString = value;
+				OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value));
+			}
+		}
+
+		/// <summary>
+		/// Invoked when the <see cref="SearchString"/> changes. Useful for debugging. Invokes the <see cref="SearchStringChanged"/> event.
+		/// </summary>
+		/// <param name="e"></param>
+		public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e)
+		{
+			SearchStringChanged?.Invoke (this, e);
+		}
+
+		/// <summary>
+		/// Gets the index of the next item in the collection that matches the current <see cref="SearchString"/> plus the provided character (typically
+		/// from a key press).
+		/// </summary>
+		/// <param name="currentIndex">The index in the collection to start the search from.</param>
+		/// <param name="keyStruck">The character of the key the user pressed.</param>
+		/// <returns>The index of the item that matches what the user has typed. 
+		/// Returns <see langword="-1"/> if no item in the collection matched.</returns>
+		public int GetNextMatchingItem (int currentIndex, char keyStruck)
+		{
+			if (!char.IsControl (keyStruck)) {
+
+				// maybe user pressed 'd' and now presses 'd' again.
+				// a candidate search is things that begin with "dd"
+				// but if we find none then we must fallback on cycling
+				// d instead and discard the candidate state
+				string candidateState = "";
+
+				// is it a second or third (etc) keystroke within a short time
+				if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
+					// "dd" is a candidate
+					candidateState = SearchString + keyStruck;
+				} else {
+					// its a fresh keystroke after some time
+					// or its first ever key press
+					SearchString = new string (keyStruck, 1);
+				}
+
+				var idxCandidate = GetNextMatchingItem (currentIndex, candidateState,
+					// prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
+					candidateState.Length > 1);
+
+				if (idxCandidate != -1) {
+					// found "dd" so candidate searchstring is accepted
+					lastKeystroke = DateTime.Now;
+					SearchString = candidateState;
+					return idxCandidate;
+				}
+
+				//// nothing matches "dd" so discard it as a candidate
+				//// and just cycle "d" instead
+				lastKeystroke = DateTime.Now;
+				idxCandidate = GetNextMatchingItem (currentIndex, candidateState);
+
+				// if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z'
+				// instead of "can" + 'd').
+				if (SearchString.Length > 1 && idxCandidate == -1) {
+					// ignore it since we're still within the typing delay
+					// don't add it to SearchString either
+					return currentIndex;
+				}
+
+				// if no changes to current state manifested
+				if (idxCandidate == currentIndex || idxCandidate == -1) {
+					// clear history and treat as a fresh letter
+					ClearSearchString ();
+
+					// match on the fresh letter alone
+					SearchString = new string (keyStruck, 1);
+					idxCandidate = GetNextMatchingItem (currentIndex, SearchString);
+					return idxCandidate == -1 ? currentIndex : idxCandidate;
+				}
+
+				// Found another "d" or just leave index as it was
+				return idxCandidate;
+
+			} else {
+				// clear state because keypress was a control char
+				ClearSearchString ();
+
+				// control char indicates no selection
+				return -1;
+			}
+		}
+
+		/// <summary>
+		/// Gets the index of the next item in the collection that matches <paramref name="search"/>. 
+		/// </summary>
+		/// <param name="currentIndex">The index in the collection to start the search from.</param>
+		/// <param name="search">The search string to use.</param>
+		/// <param name="minimizeMovement">Set to <see langword="true"/> to stop the search on the first match
+		/// if there are multiple matches for <paramref name="search"/>.
+		/// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If <see langword="false"/> (the default), 
+		/// the next matching item will be returned, even if it is above in the collection.
+		/// </param>
+		/// <returns>The index of the next matching item or <see langword="-1"/> if no match was found.</returns>
+		internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false)
+		{
+			if (string.IsNullOrEmpty (search)) {
+				return -1;
+			}
+
+			var collectionLength = GetCollectionLength ();
+
+			if (currentIndex != -1 && currentIndex < collectionLength && IsMatch (search, ElementAt (currentIndex))) {
+				// we are already at a match
+				if (minimizeMovement) {
+					// if we would rather not jump around (e.g. user is typing lots of text to get this match)
+					return currentIndex;
+				}
+
+				for (int i = 1; i < collectionLength; i++) {
+					//circular
+					var idxCandidate = (i + currentIndex) % collectionLength;
+					if (IsMatch (search, ElementAt (idxCandidate))) {
+						return idxCandidate;
+					}
+				}
+
+				// nothing else starts with the search term
+				return currentIndex;
+			} else {
+				// search terms no longer match the current selection or there is none
+				for (int i = 0; i < collectionLength; i++) {
+					if (IsMatch (search, ElementAt (i))) {
+						return i;
+					}
+				}
+
+				// Nothing matches
+				return -1;
+			}
+		}
+
+		/// <summary>
+		/// Return the number of elements in the collection
+		/// </summary>
+		protected abstract int GetCollectionLength ();
+
+		private bool IsMatch (string search, object value)
+		{
+			return value?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false;
+		}
+
+		/// <summary>
+		/// Returns the collection being navigated element at <paramref name="idx"/>.
+		/// </summary>
+		/// <returns></returns>
+		protected abstract object ElementAt (int idx);
+
+		private void ClearSearchString ()
+		{
+			SearchString = "";
+			lastKeystroke = DateTime.Now;
+		}
+
+		/// <summary>
+		/// Returns true if <paramref name="kb"/> is a searchable key
+		/// (e.g. letters, numbers, etc) that are valid to pass to this
+		/// class for search filtering.
+		/// </summary>
+		/// <param name="kb"></param>
+		/// <returns></returns>
+		public static bool IsCompatibleKey (KeyEvent kb)
+		{
+			return !kb.IsAlt && !kb.IsCtrl;
+		}
+	}
+}

+ 311 - 0
Terminal.Gui/Text/RuneExtensions.cs

@@ -0,0 +1,311 @@
+using System.Globalization;
+using System.Text;
+
+namespace Terminal.Gui;
+
+/// <summary>
+/// Extends <see cref="System.Text.Rune"/> to support TUI text manipulation.
+/// </summary>
+public static class RuneExtensions {
+	/// <summary>
+	/// Maximum Unicode code point.
+	/// </summary>
+	public static int MaxUnicodeCodePoint = 0x10FFFF;
+
+	/// <summary>
+	/// Gets the number of columns the rune occupies in the terminal.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="rune">The rune to measure.</param>
+	/// <returns>
+	/// The number of columns required to fit the rune, 0 if the argument is the null character, or
+	/// -1 if the value is not printable, 
+	/// otherwise the number of columns that the rune occupies.
+	/// </returns>
+	public static int GetColumns (this Rune rune)
+	{
+		// TODO: I believe there is a way to do this without using our own tables, using Rune.
+		var codePoint = rune.Value;
+		switch (codePoint) {
+		case < 0x20:
+		case >= 0x7f and < 0xa0:
+			return -1;
+		case < 0x7f:
+			return 1;
+		}
+		/* binary search in table of non-spacing characters */
+		if (BiSearch (codePoint, _combining, _combining.GetLength (0) - 1) != 0) {
+			return 0;
+		}
+		/* if we arrive here, ucs is not a combining or C0/C1 control character */
+		return 1 + (BiSearch (codePoint, _combiningWideChars, _combiningWideChars.GetLength (0) - 1) != 0 ? 1 : 0);
+	}
+	
+	/// <summary>
+	/// Returns <see langword="true"/> if the rune is a combining character.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="rune"></param>
+	/// <returns></returns>
+	public static bool IsCombiningMark (this System.Text.Rune rune)
+	{
+		UnicodeCategory category = Rune.GetUnicodeCategory (rune);
+		return Rune.GetUnicodeCategory (rune) == UnicodeCategory.NonSpacingMark
+			|| category == UnicodeCategory.SpacingCombiningMark
+			|| category == UnicodeCategory.EnclosingMark;
+	}
+
+	/// <summary>
+	/// Ensures the rune is not a control character and can be displayed by translating characters below 0x20
+	/// to equivalent, printable, Unicode chars.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="rune"></param>
+	/// <returns></returns>
+	public static Rune MakePrintable (this System.Text.Rune rune) => Rune.IsControl (rune) ? new Rune (rune.Value + 0x2400) : rune;
+
+	/// <summary>
+	/// Get number of bytes required to encode the rune, based on the provided encoding.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="rune">The rune to probe.</param>
+	/// <param name="encoding">The encoding used; the default is UTF8.</param>
+	/// <returns>The number of bytes required.</returns>
+	public static int GetEncodingLength (this Rune rune, Encoding encoding = null)
+	{
+		encoding ??= Encoding.UTF8;
+		var bytes = encoding.GetBytes (rune.ToString ().ToCharArray ());
+		var offset = 0;
+		if (bytes [^1] == 0) {
+			offset++;
+		}
+		return bytes.Length - offset;
+	}
+
+	/// <summary>
+	/// Writes into the destination buffer starting at offset the UTF8 encoded version of the rune.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="rune">The rune to encode.</param>
+	/// <param name="dest">The destination buffer.</param>
+	/// <param name="start">Starting offset to look into.</param>
+	/// <param name="count">Number of bytes valid in the buffer, or -1 to make it the length of the buffer.</param>
+	/// <returns>he number of bytes written into the destination buffer.</returns>
+	public static int Encode (this Rune rune, byte [] dest, int start = 0, int count = -1)
+	{
+		var bytes = Encoding.UTF8.GetBytes (rune.ToString ());
+		var length = 0;
+		for (var i = 0; i < (count == -1 ? bytes.Length : count); i++) {
+			if (bytes [i] == 0) {
+				break;
+			}
+			dest [start + i] = bytes [i];
+			length++;
+		}
+		return length;
+	}
+
+	/// <summary>
+	/// Attempts to decode the rune as a surrogate pair to UTF-16.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="rune">The rune to decode.</param>
+	/// <param name="chars">The chars if the rune is a surrogate pair. Null otherwise.</param>
+	/// <returns><see langword="true"/> if the rune is a valid surrogate pair; <see langword="false"/> otherwise.</returns>
+	public static bool DecodeSurrogatePair (this Rune rune, out char [] chars)
+	{
+		if (rune.IsSurrogatePair ()) {
+			chars = rune.ToString ().ToCharArray ();
+			return true;
+		}
+		chars = null;
+		return false;
+	}
+
+	/// <summary>
+	/// Attempts to encode (as UTF-16) a surrogate pair.
+	/// </summary>
+	/// <param name="highSurrogate">The high surrogate code point.</param>
+	/// <param name="lowSurrogate">The low surrogate code point.</param>
+	/// <param name="result">The encoded rune.</param>
+	/// <returns><see langword="true"/> if the encoding succeeded; <see langword="false"/> otherwise.</returns>
+	public static bool EncodeSurrogatePair (char highSurrogate, char lowSurrogate, out Rune result)
+	{
+		result = default;
+		if (char.IsSurrogatePair (highSurrogate, lowSurrogate)) {
+			result = (Rune)char.ConvertToUtf32 (highSurrogate, lowSurrogate);
+			return true;
+		}
+		return false;
+	}
+
+	/// <summary>
+	/// Reports whether a rune is a surrogate code point.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="System.Text.Rune"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="rune">The rune to probe.</param>
+	/// <returns><see langword="true"/> if the rune is a surrogate code point; <see langword="false"/> otherwise.</returns>
+	public static bool IsSurrogatePair (this Rune rune)
+	{
+		return char.IsSurrogatePair (rune.ToString (), 0);
+	}
+
+	/// <summary>
+	/// Reports if the provided array of bytes can be encoded as UTF-8.
+	/// </summary>
+	/// <param name="buffer">The byte array to probe.</param>
+	/// <value><c>true</c> if is valid; otherwise, <c>false</c>.</value>
+	public static bool CanBeEncodedAsRune (byte [] buffer)
+	{
+		var str = Encoding.Unicode.GetString (buffer);
+		foreach (var rune in str.EnumerateRunes ()) {
+			if (rune == Rune.ReplacementChar) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	// ---------------- implementation details ------------------
+	// TODO: Can this be handled by the new .NET 8 Rune type?
+	static readonly int [,] _combining = new int [,] {
+		{ 0x0300, 0x036F }, { 0x0483, 0x0486 }, { 0x0488, 0x0489 },
+		{ 0x0591, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 },
+		{ 0x05C4, 0x05C5 }, { 0x05C7, 0x05C7 }, { 0x0600, 0x0603 },
+		{ 0x0610, 0x0615 }, { 0x064B, 0x065E }, { 0x0670, 0x0670 },
+		{ 0x06D6, 0x06E4 }, { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED },
+		{ 0x070F, 0x070F }, { 0x0711, 0x0711 }, { 0x0730, 0x074A },
+		{ 0x07A6, 0x07B0 }, { 0x07EB, 0x07F3 }, { 0x0901, 0x0902 },
+		{ 0x093C, 0x093C }, { 0x0941, 0x0948 }, { 0x094D, 0x094D },
+		{ 0x0951, 0x0954 }, { 0x0962, 0x0963 }, { 0x0981, 0x0981 },
+		{ 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, { 0x09CD, 0x09CD },
+		{ 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, { 0x0A3C, 0x0A3C },
+		{ 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D },
+		{ 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, { 0x0ABC, 0x0ABC },
+		{ 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, { 0x0ACD, 0x0ACD },
+		{ 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, { 0x0B3C, 0x0B3C },
+		{ 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, { 0x0B4D, 0x0B4D },
+		{ 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, { 0x0BC0, 0x0BC0 },
+		{ 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, { 0x0C46, 0x0C48 },
+		{ 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, { 0x0CBC, 0x0CBC },
+		{ 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, { 0x0CCC, 0x0CCD },
+		{ 0x0CE2, 0x0CE3 }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D },
+		{ 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 },
+		{ 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E },
+		{ 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC },
+		{ 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 },
+		{ 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E },
+		{ 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 },
+		{ 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 },
+		{ 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 },
+		{ 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x135F, 0x135F },
+		{ 0x1712, 0x1714 }, { 0x1732, 0x1734 }, { 0x1752, 0x1753 },
+		{ 0x1772, 0x1773 }, { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD },
+		{ 0x17C6, 0x17C6 }, { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD },
+		{ 0x180B, 0x180D }, { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 },
+		{ 0x1927, 0x1928 }, { 0x1932, 0x1932 }, { 0x1939, 0x193B },
+		{ 0x1A17, 0x1A18 }, { 0x1B00, 0x1B03 }, { 0x1B34, 0x1B34 },
+		{ 0x1B36, 0x1B3A }, { 0x1B3C, 0x1B3C }, { 0x1B42, 0x1B42 },
+		{ 0x1B6B, 0x1B73 }, { 0x1DC0, 0x1DCA }, { 0x1DFE, 0x1DFF },
+		{ 0x200B, 0x200F }, { 0x202A, 0x202E }, { 0x2060, 0x2063 },
+		{ 0x206A, 0x206F }, { 0x20D0, 0x20EF }, { 0x2E9A, 0x2E9A },
+		{ 0x2EF4, 0x2EFF }, { 0x2FD6, 0x2FEF }, { 0x2FFC, 0x2FFF },
+		{ 0x31E4, 0x31EF }, { 0x321F, 0x321F }, { 0xA48D, 0xA48F },
+		{ 0xA806, 0xA806 }, { 0xA80B, 0xA80B }, { 0xA825, 0xA826 },
+		{ 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F }, { 0xFE1A, 0xFE1F },
+		{ 0xFE20, 0xFE23 }, { 0xFE53, 0xFE53 }, { 0xFE67, 0xFE67 },
+		{ 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB },
+		{ 0x10A01, 0x10A03 }, { 0x10A05, 0x10A06 }, { 0x10A0C, 0x10A0F },
+		{ 0x10A38, 0x10A3A }, { 0x10A3F, 0x10A3F }, { 0x1D167, 0x1D169 },
+		{ 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, { 0x1D1AA, 0x1D1AD },
+		{ 0x1D242, 0x1D244 }, { 0xE0001, 0xE0001 }, { 0xE0020, 0xE007F },
+		{ 0xE0100, 0xE01EF }
+	};
+
+	static readonly int [,] _combiningWideChars = new int [,] {
+		/* Hangul Jamo init. consonants - 0x1100, 0x11ff */
+		/* Miscellaneous Technical - 0x2300, 0x23ff */
+		/* Hangul Syllables - 0x11a8, 0x11c2 */
+		/* CJK Compatibility Ideographs - f900, fad9 */
+		/* Vertical forms - fe10, fe19 */
+		/* CJK Compatibility Forms - fe30, fe4f */
+		/* Fullwidth Forms - ff01, ffee */
+		/* Alphabetic Presentation Forms - 0xFB00, 0xFb4f */
+		/* Chess Symbols - 0x1FA00, 0x1FA0f */
+
+		{ 0x1100, 0x115f }, { 0x231a, 0x231b }, { 0x2329, 0x232a },
+		{ 0x23e9, 0x23ec }, { 0x23f0, 0x23f0 }, { 0x23f3, 0x23f3 },
+		{ 0x25fd, 0x25fe }, { 0x2614, 0x2615 }, { 0x2648, 0x2653 },
+		{ 0x267f, 0x267f }, { 0x2693, 0x2693 }, { 0x26a1, 0x26a1 },
+		{ 0x26aa, 0x26ab }, { 0x26bd, 0x26be }, { 0x26c4, 0x26c5 },
+		{ 0x26ce, 0x26ce }, { 0x26d4, 0x26d4 }, { 0x26ea, 0x26ea },
+		{ 0x26f2, 0x26f3 }, { 0x26f5, 0x26f5 }, { 0x26fa, 0x26fa },
+		{ 0x26fd, 0x26fd }, { 0x2705, 0x2705 }, { 0x270a, 0x270b },
+		{ 0x2728, 0x2728 }, { 0x274c, 0x274c }, { 0x274e, 0x274e },
+		{ 0x2753, 0x2755 }, { 0x2757, 0x2757 }, { 0x2795, 0x2797 },
+		{ 0x27b0, 0x27b0 }, { 0x27bf, 0x27bf }, { 0x2b1b, 0x2b1c },
+		{ 0x2b50, 0x2b50 }, { 0x2b55, 0x2b55 }, { 0x2e80, 0x303e },
+		{ 0x3041, 0x3096 }, { 0x3099, 0x30ff }, { 0x3105, 0x312f },
+		{ 0x3131, 0x318e }, { 0x3190, 0x3247 }, { 0x3250, 0x4dbf },
+		{ 0x4e00, 0xa4c6 }, { 0xa960, 0xa97c }, { 0xac00, 0xd7a3 },
+		{ 0xf900, 0xfaff }, { 0xfe10, 0xfe1f }, { 0xfe30, 0xfe6b },
+		{ 0xff01, 0xff60 }, { 0xffe0, 0xffe6 },
+		{ 0x16fe0, 0x16fe4 }, { 0x16ff0, 0x16ff1 }, { 0x17000, 0x187f7 },
+		{ 0x18800, 0x18cd5 }, { 0x18d00, 0x18d08 }, { 0x1aff0, 0x1affc },
+		{ 0x1b000, 0x1b122 }, { 0x1b150, 0x1b152 }, { 0x1b164, 0x1b167 }, { 0x1b170, 0x1b2fb }, { 0x1d538, 0x1d550 },
+		{ 0x1f004, 0x1f004 }, { 0x1f0cf, 0x1f0cf }, /*{ 0x1f100, 0x1f10a },*/
+		//{ 0x1f110, 0x1f12d }, { 0x1f130, 0x1f169 }, { 0x1f170, 0x1f1ac },
+		{ 0x1f18f, 0x1f199 },
+		{ 0x1f1e6, 0x1f1ff }, { 0x1f200, 0x1f202 }, { 0x1f210, 0x1f23b },
+		{ 0x1f240, 0x1f248 }, { 0x1f250, 0x1f251 }, { 0x1f260, 0x1f265 },
+		{ 0x1f300, 0x1f320 }, { 0x1f32d, 0x1f33e }, { 0x1f340, 0x1f37e },
+		{ 0x1f380, 0x1f393 }, { 0x1f3a0, 0x1f3ca }, { 0x1f3cf, 0x1f3d3 },
+		{ 0x1f3e0, 0x1f3f0 }, { 0x1f3f4, 0x1f3f4 }, { 0x1f3f8, 0x1f43e },
+		{ 0x1f440, 0x1f44e }, { 0x1f450, 0x1f4fc }, { 0x1f4ff, 0x1f53d },
+		{ 0x1f54b, 0x1f54e }, { 0x1f550, 0x1f567 }, { 0x1f57a, 0x1f57a },
+		{ 0x1f595, 0x1f596 }, { 0x1f5a4, 0x1f5a4 }, { 0x1f5fb, 0x1f606 },
+		{ 0x1f607, 0x1f64f }, { 0x1f680, 0x1f6c5 }, { 0x1f6cc, 0x1f6cc },
+		{ 0x1f6d0, 0x1f6d2 }, { 0x1f6d5, 0x1f6d7 }, { 0x1f6dd, 0x1f6df }, { 0x1f6eb, 0x1f6ec },
+		{ 0x1f6f4, 0x1f6fc }, { 0x1f7e0, 0x1f7eb }, { 0x1f7f0, 0x1f7f0 }, { 0x1f90c, 0x1f93a },
+		{ 0x1f93c, 0x1f945 }, { 0x1f947, 0x1f97f }, { 0x1f980, 0x1f9cc },
+		{ 0x1f9cd, 0x1f9ff }, { 0x1fa70, 0x1fa74 }, { 0x1fa78, 0x1fa7c }, { 0x1fa80, 0x1fa86 },
+		{ 0x1fa90, 0x1faac }, { 0x1fab0, 0x1faba }, { 0x1fac0, 0x1fac5 },
+		{ 0x1fad0, 0x1fad9 }, { 0x1fae0, 0x1fae7 }, { 0x1faf0, 0x1faf6 }, { 0x20000, 0x2fffd }, { 0x30000, 0x3fffd },
+		//{ 0xe0100, 0xe01ef }, { 0xf0000, 0xffffd }, { 0x100000, 0x10fffd }
+	};
+
+	static int BiSearch (int rune, int [,] table, int max)
+	{
+		var min = 0;
+
+		if (rune < table [0, 0] || rune > table [max, 1]) {
+			return 0;
+		}
+		while (max >= min) {
+			var mid = (min + max) / 2;
+			if (rune > table [mid, 1]) {
+				min = mid + 1;
+			} else if (rune < table [mid, 0]) {
+				max = mid - 1;
+			} else {
+				return 1;
+			}
+		}
+
+		return 0;
+	}
+}

+ 154 - 0
Terminal.Gui/Text/StringExtensions.cs

@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Terminal.Gui;
+/// <summary>
+/// Extensions to <see cref="string"/> to support TUI text manipulation.
+/// </summary>
+public static class StringExtensions {
+	/// <summary>
+	/// Repeats the string <paramref name="n"/> times.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="str">The text to repeat.</param>
+	/// <param name="n">Number of times to repeat the text.</param>
+	/// <returns>
+	///  The text repeated if <paramref name="n"/> is greater than zero, 
+	///  otherwise <see langword="null"/>.
+	/// </returns>
+	public static string Repeat (this string str, int n)
+	{
+		if (n <= 0) {
+			return null;
+		}
+
+		if (string.IsNullOrEmpty (str) || n == 1) {
+			return str;
+		}
+
+		return new StringBuilder (str.Length * n)
+			.Insert (0, str, n)
+			.ToString ();
+	}
+
+	/// <summary>
+	/// Gets the number of columns the string occupies in the terminal.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="str">The string to measure.</param>
+	/// <returns></returns>
+	public static int GetColumns (this string str)
+	{
+		return str == null ? 0 : str.EnumerateRunes ().Sum (r => Math.Max (r.GetColumns (), 0));
+	}
+
+	/// <summary>
+	/// Gets the number of runes in the string.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="str">The string to count.</param>
+	/// <returns></returns>
+	public static int GetRuneCount (this string str) => str.EnumerateRunes ().Count ();
+
+	/// <summary>
+	/// Converts the string into a <see cref="Rune"/> array.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="str">The string to convert.</param>
+	/// <returns></returns>
+	public static Rune [] ToRunes (this string str) => str.EnumerateRunes ().ToArray ();
+
+	/// <summary>
+	/// Converts the string into a <see cref="List{Rune}"/>.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="str">The string to convert.</param>
+	/// <returns></returns>
+	public static List<Rune> ToRuneList (this string str) => str.EnumerateRunes ().ToList ();
+
+	/// <summary>
+	/// Unpacks the first UTF-8 encoding in the string and returns the rune and its width in bytes.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="str">The string to decode.</param>
+	/// <param name="start">Starting offset.</param>
+	/// <param name="count">Number of bytes in the buffer, or -1 to make it the length of the buffer.</param>
+	/// <returns></returns>
+	public static (Rune Rune, int Size) DecodeRune (this string str, int start = 0, int count = -1)
+	{
+		var rune = str.EnumerateRunes ().ToArray () [start];
+		var bytes = Encoding.UTF8.GetBytes (rune.ToString ());
+		if (count == -1) {
+			count = bytes.Length;
+		}
+		var operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
+		if (operationStatus == System.Buffers.OperationStatus.Done && bytesConsumed >= count) {
+			return (rune, bytesConsumed);
+		}
+		return (Rune.ReplacementChar, 1);
+	}
+
+	/// <summary>
+	/// Unpacks the last UTF-8 encoding in the string.
+	/// </summary>
+	/// <remarks>
+	/// This is a Terminal.Gui extension method to <see cref="string"/> to support TUI text manipulation.
+	/// </remarks>
+	/// <param name="str">The string to decode.</param>
+	/// <param name="end">Index in string to stop at; if -1, use the buffer length.</param>
+	/// <returns></returns>
+	public static (Rune rune, int size) DecodeLastRune (this string str, int end = -1)
+	{
+		var rune = str.EnumerateRunes ().ToArray () [end == -1 ? ^1 : end];
+		var bytes = Encoding.UTF8.GetBytes (rune.ToString ());
+		var operationStatus = Rune.DecodeFromUtf8 (bytes, out rune, out int bytesConsumed);
+		if (operationStatus == System.Buffers.OperationStatus.Done) {
+			return (rune, bytesConsumed);
+		}
+		return (Rune.ReplacementChar, 1);
+	}
+
+	/// <summary>
+	/// Converts a <see cref="Rune"/> generic collection into a string.
+	/// </summary>
+	/// <param name="runes">The enumerable rune to convert.</param>
+	/// <returns></returns>
+	public static string ToString (IEnumerable<Rune> runes)
+	{
+		var str = string.Empty;
+
+		foreach (var rune in runes) {
+			str += rune.ToString ();
+		}
+
+		return str;
+	}
+
+	/// <summary>
+	/// Converts a byte generic collection into a string in the provided encoding (default is UTF8)
+	/// </summary>
+	/// <param name="bytes">The enumerable byte to convert.</param>
+	/// <param name="encoding">The encoding to be used.</param>
+	/// <returns></returns>
+	public static string ToString (IEnumerable<byte> bytes, Encoding encoding = null)
+	{
+		if (encoding == null) {
+			encoding = Encoding.UTF8;
+		}
+		return encoding.GetString (bytes.ToArray ());
+	}
+}

+ 35 - 0
Terminal.Gui/Text/TableCollectionNavigator.cs

@@ -0,0 +1,35 @@
+
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Collection navigator for cycling selections in a <see cref="TableView"/>.
+	/// </summary>
+	public class TableCollectionNavigator : CollectionNavigatorBase {
+		readonly TableView tableView;
+
+		/// <summary>
+		/// Creates a new instance for navigating the data in the wrapped <paramref name="tableView"/>.
+		/// </summary>
+		public TableCollectionNavigator (TableView tableView)
+		{
+			this.tableView = tableView;
+		}
+
+		/// <inheritdoc/>
+		protected override object ElementAt (int idx)
+		{
+			var col = tableView.SelectedColumn;
+			var rawValue = tableView.Table [idx, col];
+
+			var style = this.tableView.Style.GetColumnStyleIfAny (col);
+			return style?.RepresentationGetter?.Invoke (rawValue) ?? rawValue;
+		}
+
+		/// <inheritdoc/>
+		protected override int GetCollectionLength ()
+		{
+			return tableView.Table.Rows;
+		}
+	}
+}

+ 143 - 145
Terminal.Gui/Text/TextFormatter.cs

@@ -3,8 +3,6 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Text;
-using NStack;
-using Rune = System.Rune;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -12,19 +10,20 @@ namespace Terminal.Gui {
 	/// </summary>
 	public enum TextAlignment {
 		/// <summary>
-		/// Aligns the text to the left of the frame.
+		/// The text will be left-aligned.
 		/// </summary>
 		Left,
 		/// <summary>
-		/// Aligns the text to the right side of the frame.
+		/// The text will be right-aligned.
 		/// </summary>
 		Right,
 		/// <summary>
-		/// Centers the text in the frame.
+		/// The text will be centered horizontally.
 		/// </summary>
 		Centered,
 		/// <summary>
-		/// Shows the text as justified text in the frame.
+		/// The text will be justified (spaces will be added to existing spaces such that
+		/// the text fills the container horizontally).
 		/// </summary>
 		Justified
 	}
@@ -34,19 +33,20 @@ namespace Terminal.Gui {
 	/// </summary>
 	public enum VerticalTextAlignment {
 		/// <summary>
-		/// Aligns the text to the top of the frame.
+		/// The text will be top-aligned.
 		/// </summary>
 		Top,
 		/// <summary>
-		/// Aligns the text to the bottom of the frame.
+		/// The text will be bottom-aligned.
 		/// </summary>
 		Bottom,
 		/// <summary>
-		/// Centers the text verticaly in the frame.
+		/// The text will centered vertically.
 		/// </summary>
 		Middle,
 		/// <summary>
-		/// Shows the text as justified text in the frame.
+		/// The text will be justified (spaces will be added to existing spaces such that
+		/// the text fills the container vertically).
 		/// </summary>
 		Justified
 	}
@@ -112,17 +112,17 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// Provides text formatting capabilities for console apps. Supports, hotkeys, horizontal alignment, multiple lines, and word-based line wrap.
+	/// Provides text formatting. Supports <see cref="View.HotKey"/>s, horizontal alignment, vertical alignment, multiple lines, and word-based line wrap.
 	/// </summary>
 	public class TextFormatter {
 
 		#region Static Members
 
-		static ustring StripCRLF (ustring str, bool keepNewLine = false)
+		static string StripCRLF (string str, bool keepNewLine = false)
 		{
 			var runes = str.ToRuneList ();
 			for (int i = 0; i < runes.Count; i++) {
-				switch (runes [i]) {
+				switch ((char)runes [i].Value) {
 				case '\n':
 					if (!keepNewLine) {
 						runes.RemoveAt (i);
@@ -130,7 +130,7 @@ namespace Terminal.Gui {
 					break;
 
 				case '\r':
-					if ((i + 1) < runes.Count && runes [i + 1] == '\n') {
+					if ((i + 1) < runes.Count && runes [i + 1].Value == '\n') {
 						runes.RemoveAt (i);
 						if (!keepNewLine) {
 							runes.RemoveAt (i);
@@ -144,19 +144,19 @@ namespace Terminal.Gui {
 					break;
 				}
 			}
-			return ustring.Make (runes);
+			return StringExtensions.ToString (runes);
 		}
-		static ustring ReplaceCRLFWithSpace (ustring str)
+		static string ReplaceCRLFWithSpace (string str)
 		{
 			var runes = str.ToRuneList ();
 			for (int i = 0; i < runes.Count; i++) {
-				switch (runes [i]) {
+				switch (runes [i].Value) {
 				case '\n':
 					runes [i] = (Rune)' ';
 					break;
 
 				case '\r':
-					if ((i + 1) < runes.Count && runes [i + 1] == '\n') {
+					if ((i + 1) < runes.Count && runes [i + 1].Value == '\n') {
 						runes [i] = (Rune)' ';
 						runes.RemoveAt (i + 1);
 						i++;
@@ -166,7 +166,7 @@ namespace Terminal.Gui {
 					break;
 				}
 			}
-			return ustring.Make (runes);
+			return StringExtensions.ToString (runes);
 		}
 
 		/// <summary>
@@ -175,29 +175,29 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="text">The text.</param>
 		/// <returns>A list of text without the newline characters.</returns>
-		public static List<ustring> SplitNewLine (ustring text)
+		public static List<string> SplitNewLine (string text)
 		{
 			var runes = text.ToRuneList ();
-			var lines = new List<ustring> ();
+			var lines = new List<string> ();
 			var start = 0;
 			var end = 0;
 
 			for (int i = 0; i < runes.Count; i++) {
 				end = i;
-				switch (runes [i]) {
+				switch (runes [i].Value) {
 				case '\n':
-					lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+					lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
 					i++;
 					start = i;
 					break;
 
 				case '\r':
-					if ((i + 1) < runes.Count && runes [i + 1] == '\n') {
-						lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+					if ((i + 1) < runes.Count && runes [i + 1].Value == '\n') {
+						lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
 						i += 2;
 						start = i;
 					} else {
-						lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+						lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
 						i++;
 						start = i;
 					}
@@ -205,11 +205,11 @@ namespace Terminal.Gui {
 				}
 			}
 			if (runes.Count > 0 && lines.Count == 0) {
-				lines.Add (ustring.Make (runes));
+				lines.Add (StringExtensions.ToString (runes));
 			} else if (runes.Count > 0 && start < runes.Count) {
-				lines.Add (ustring.Make (runes.GetRange (start, runes.Count - start)));
+				lines.Add (StringExtensions.ToString (runes.GetRange (start, runes.Count - start)));
 			} else {
-				lines.Add (ustring.Make (""));
+				lines.Add ("");
 			}
 			return lines;
 		}
@@ -228,16 +228,16 @@ namespace Terminal.Gui {
 				return text;
 
 			// if value is not wide enough
-			if (text.Sum (c => Rune.ColumnWidth (c)) < width) {
+			if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width) {
 
 				// pad it out with spaces to the given alignment
-				int toPad = width - (text.Sum (c => Rune.ColumnWidth (c)));
+				int toPad = width - (text.EnumerateRunes ().Sum (c => c.GetColumns ()));
 
 				return text + new string (' ', toPad);
 			}
 
 			// value is too wide
-			return new string (text.TakeWhile (c => (width -= Rune.ColumnWidth (c)) >= 0).ToArray ());
+			return new string (text.TakeWhile (c => (width -= ((Rune)c).GetColumns ()) >= 0).ToArray ());
 		}
 
 		/// <summary>
@@ -261,7 +261,7 @@ namespace Terminal.Gui {
 		/// If <paramref name="preserveTrailingSpaces"/> is <see langword="false"/> at most one space will be preserved at the end of the last line.
 		/// </para>
 		/// </remarks>
-		public static List<ustring> WordWrapText (ustring text, int width, bool preserveTrailingSpaces = false, int tabWidth = 0,
+		public static List<string> WordWrapText (string text, int width, bool preserveTrailingSpaces = false, int tabWidth = 0,
 			TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
@@ -269,9 +269,9 @@ namespace Terminal.Gui {
 			}
 
 			int start = 0, end;
-			var lines = new List<ustring> ();
+			var lines = new List<string> ();
 
-			if (ustring.IsNullOrEmpty (text)) {
+			if (string.IsNullOrEmpty (text)) {
 				return lines;
 			}
 
@@ -280,13 +280,13 @@ namespace Terminal.Gui {
 				while ((end = start) < runes.Count) {
 					end = GetNextWhiteSpace (start, width, out bool incomplete);
 					if (end == 0 && incomplete) {
-						start = text.RuneCount;
+						start = text.GetRuneCount ();
 						break;
 					}
-					lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+					lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
 					start = end;
 					if (incomplete) {
-						start = text.RuneCount;
+						start = text.GetRuneCount ();
 						break;
 					}
 				}
@@ -333,7 +333,7 @@ namespace Terminal.Gui {
 					//		//if (line [0] == ' ' && (lines.Count > 0 && lines [lines.Count - 1] [0] == ' ')) {
 					//		//} else {
 					//		//}
-					//		lines.Add (ustring.Make (line));
+					//		lines.Add (string.Make (line));
 
 					//		// move forward to next non-space
 					//		while (width > 1 && start < runes.Count && runes [start] == ' ') {
@@ -343,28 +343,34 @@ namespace Terminal.Gui {
 					//}
 
 					while ((end = start + Math.Max (GetLengthThatFits (runes.GetRange (start, runes.Count - start), width), 1)) < runes.Count) {
-						while (runes [end] != ' ' && end > start)
+						while (runes [end].Value != ' ' && end > start)
 							end--;
 						if (end == start)
 							end = start + GetLengthThatFits (runes.GetRange (end, runes.Count - end), width);
-						lines.Add (ustring.Make (runes.GetRange (start, end - start)));
-						start = end;
-						if (runes [end] == ' ') {
-							start++;
+						var str = StringExtensions.ToString (runes.GetRange (start, end - start));
+						if (end > start && str.GetColumns () <= width) {
+							lines.Add (str);
+							start = end;
+							if (runes [end].Value == ' ') {
+								start++;
+							}
+						} else {
+							end++;
+							start = end;
 						}
 					}
-					
+
 				} else {
 					while ((end = start + width) < runes.Count) {
-						while (runes [end] != ' ' && end > start) {
+						while (runes [end].Value != ' ' && end > start) {
 							end--;
 						}
 						if (end == start) {
 							end = start + width;
 						}
-						lines.Add (ustring.Make (runes.GetRange (start, end - start)));
+						lines.Add (StringExtensions.ToString (runes.GetRange (start, end - start)));
 						start = end;
-						if (runes [end] == ' ') {
+						if (runes [end].Value == ' ') {
 							start++;
 						}
 					}
@@ -381,7 +387,7 @@ namespace Terminal.Gui {
 				while (length < cWidth && to < runes.Count) {
 					var rune = runes [to];
 					if (IsHorizontalDirection (textDirection)) {
-						length += Rune.ColumnWidth (rune);
+						length += rune.GetColumns ();
 					} else {
 						length++;
 					}
@@ -391,7 +397,7 @@ namespace Terminal.Gui {
 						}
 						return to;
 					}
-					if (rune == ' ') {
+					if (rune.Value == ' ') {
 						if (length == cWidth) {
 							return to + 1;
 						} else if (length > cWidth) {
@@ -399,7 +405,7 @@ namespace Terminal.Gui {
 						} else {
 							return GetNextWhiteSpace (to + 1, cWidth, out incomplete, length);
 						}
-					} else if (rune == '\t') {
+					} else if (rune.Value == '\t') {
 						length += tabWidth + 1;
 						if (length == tabWidth && tabWidth > cWidth) {
 							return to + 1;
@@ -411,17 +417,20 @@ namespace Terminal.Gui {
 					}
 					to++;
 				}
-				if (cLength > 0 && to < runes.Count && runes [to] != ' ' && runes [to] != '\t') {
+				if (cLength > 0 && to < runes.Count && runes [to].Value != ' ' && runes [to].Value != '\t') {
 					return from;
-				} else if (cLength > 0 && to < runes.Count && (runes [to] == ' ' || runes [to] == '\t')) {
+				} else if (cLength > 0 && to < runes.Count && (runes [to].Value == ' ' || runes [to].Value == '\t')) {
 					return lastFrom;
 				} else {
 					return to;
 				}
 			}
 
-			if (start < text.RuneCount) {
-				lines.Add (ustring.Make (runes.GetRange (start, runes.Count - start)));
+			if (start < text.GetRuneCount ()) {
+				var str = StringExtensions.ToString (runes.GetRange (start, runes.Count - start));
+				if (IsVerticalDirection (textDirection) || preserveTrailingSpaces || (!preserveTrailingSpaces && str.GetColumns () <= width)) {
+					lines.Add (str);
+				}
 			}
 
 			return lines;
@@ -435,7 +444,7 @@ namespace Terminal.Gui {
 		/// <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, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
+		public static string ClipAndJustify (string text, int width, TextAlignment talign, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection);
 		}
@@ -448,12 +457,12 @@ namespace Terminal.Gui {
 		/// <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, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
+		public static string ClipAndJustify (string text, int width, bool justify, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("Width cannot be negative.");
 			}
-			if (ustring.IsNullOrEmpty (text)) {
+			if (string.IsNullOrEmpty (text)) {
 				return text;
 			}
 
@@ -461,15 +470,15 @@ namespace Terminal.Gui {
 			int slen = runes.Count;
 			if (slen > width) {
 				if (IsHorizontalDirection (textDirection)) {
-					return ustring.Make (runes.GetRange (0, GetLengthThatFits (text, width)));
+					return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width)));
 				} else {
-					return ustring.Make (runes.GetRange (0, width));
+					return StringExtensions.ToString (runes.GetRange (0, width));
 				}
 			} else {
 				if (justify) {
 					return Justify (text, width, ' ', textDirection);
-				} else if (IsHorizontalDirection (textDirection) && GetTextWidth (text) > width) {
-					return ustring.Make (runes.GetRange (0, GetLengthThatFits (text, width)));
+				} else if (IsHorizontalDirection (textDirection) && text.GetColumns () > width) {
+					return StringExtensions.ToString (runes.GetRange (0, GetLengthThatFits (text, width)));
 				}
 				return text;
 			}
@@ -484,21 +493,21 @@ namespace Terminal.Gui {
 		/// <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 = ' ', TextDirection textDirection = TextDirection.LeftRight_TopBottom)
+		public static string Justify (string text, int width, char spaceChar = ' ', TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("Width cannot be negative.");
 			}
-			if (ustring.IsNullOrEmpty (text)) {
+			if (string.IsNullOrEmpty (text)) {
 				return text;
 			}
 
-			var words = text.Split (ustring.Make (' '));
+			var words = text.Split (' ');
 			int textCount;
 			if (IsHorizontalDirection (textDirection)) {
-				textCount = words.Sum (arg => GetTextWidth (arg));
+				textCount = words.Sum (arg => arg.GetColumns ());
 			} else {
-				textCount = words.Sum (arg => arg.RuneCount);
+				textCount = words.Sum (arg => arg.GetRuneCount ());
 			}
 			var spaces = words.Length > 1 ? (width - textCount) / (words.Length - 1) : 0;
 			var extras = words.Length > 1 ? (width - textCount) % (words.Length - 1) : 0;
@@ -520,7 +529,7 @@ namespace Terminal.Gui {
 						s.Append (spaceChar);
 				}
 			}
-			return ustring.Make (s.ToString ());
+			return s.ToString ();
 		}
 
 		static char [] whitespace = new char [] { ' ', '\t' };
@@ -549,7 +558,7 @@ namespace Terminal.Gui {
 		/// If <paramref name="width"/> 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, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
+		public static List<string> Format (string 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, textDirection);
 		}
@@ -578,16 +587,16 @@ namespace Terminal.Gui {
 		/// If <paramref name="width"/> is int.MaxValue, the text will be formatted to the maximum width possible. 
 		/// </para>
 		/// </remarks>
-		public static List<ustring> Format (ustring text, int width, bool justify, bool wordWrap,
+		public static List<string> Format (string text, int width, bool justify, bool wordWrap,
 			bool preserveTrailingSpaces = false, int tabWidth = 0, TextDirection textDirection = TextDirection.LeftRight_TopBottom)
 		{
 			if (width < 0) {
 				throw new ArgumentOutOfRangeException ("width cannot be negative");
 			}
-			List<ustring> lineResult = new List<ustring> ();
+			List<string> lineResult = new List<string> ();
 
-			if (ustring.IsNullOrEmpty (text) || width == 0) {
-				lineResult.Add (ustring.Empty);
+			if (string.IsNullOrEmpty (text) || width == 0) {
+				lineResult.Add (string.Empty);
 				return lineResult;
 			}
 
@@ -602,18 +611,18 @@ namespace Terminal.Gui {
 			int lp = 0;
 			for (int i = 0; i < runeCount; i++) {
 				Rune c = runes [i];
-				if (c == '\n') {
-					var wrappedLines = WordWrapText (ustring.Make (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces, tabWidth, textDirection);
+				if (c.Value == '\n') {
+					var wrappedLines = WordWrapText (StringExtensions.ToString (runes.GetRange (lp, i - lp)), width, preserveTrailingSpaces, tabWidth, textDirection);
 					foreach (var line in wrappedLines) {
 						lineResult.Add (ClipAndJustify (line, width, justify, textDirection));
 					}
 					if (wrappedLines.Count == 0) {
-						lineResult.Add (ustring.Empty);
+						lineResult.Add (string.Empty);
 					}
 					lp = i + 1;
 				}
 			}
-			foreach (var line in WordWrapText (ustring.Make (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces, tabWidth, textDirection)) {
+			foreach (var line in WordWrapText (StringExtensions.ToString (runes.GetRange (lp, runeCount - lp)), width, preserveTrailingSpaces, tabWidth, textDirection)) {
 				lineResult.Add (ClipAndJustify (line, width, justify, textDirection));
 			}
 
@@ -626,7 +635,7 @@ namespace Terminal.Gui {
 		/// <returns>Number of lines.</returns>
 		/// <param name="text">Text, may contain newlines.</param>
 		/// <param name="width">The minimum width for the text.</param>
-		public static int MaxLines (ustring text, int width)
+		public static int MaxLines (string text, int width)
 		{
 			var result = TextFormatter.Format (text, width, false, true);
 			return result.Count;
@@ -639,13 +648,13 @@ namespace Terminal.Gui {
 		/// <returns>Width of the longest line after formatting the text constrained by <paramref name="maxColumns"/>.</returns>
 		/// <param name="text">Text, may contain newlines.</param>
 		/// <param name="maxColumns">The number of columns to constrain the text to for formatting.</param>
-		public static int MaxWidth (ustring text, int maxColumns)
+		public static int MaxWidth (string text, int maxColumns)
 		{
 			var result = TextFormatter.Format (text: text, width: maxColumns, justify: false, wordWrap: true);
 			var max = 0;
 			result.ForEach (s => {
 				var m = 0;
-				s.ToRuneList ().ForEach (r => m += Math.Max (Rune.ColumnWidth (r), 1));
+				s.ToRuneList ().ForEach (r => m += Math.Max (r.GetColumns (), 1));
 				if (m > max) {
 					max = m;
 				}
@@ -654,25 +663,15 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Returns the width of the widest line in the text, accounting for wide-glyphs (uses <see cref="ustring.ConsoleWidth"/>).
+		/// Returns the width of the widest line in the text, accounting for wide-glyphs (uses <see cref="StringExtensions.GetColumns"/>).
 		/// <paramref name="text"/> if it contains newlines.
 		/// </summary>
 		/// <param name="text">Text, may contain newlines.</param>
 		/// <returns>The length of the longest line.</returns>
-		public static int MaxWidthLine (ustring text)
+		public static int MaxWidthLine (string text)
 		{
 			var result = TextFormatter.SplitNewLine (text);
-			return result.Max (x => x.ConsoleWidth);
-		}
-
-		/// <summary>
-		/// Gets the number of columns the passed text will use, ignoring newlines and accounting for wide-glyphs (uses <see cref="ustring.ConsoleWidth"/>).
-		/// </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));
+			return result.Max (x => x.GetColumns ());
 		}
 
 		/// <summary>
@@ -683,13 +682,13 @@ namespace Terminal.Gui {
 		/// <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)
+		public static int GetSumMaxCharWidth (List<string> 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));
+					max += runes.EnumerateRunes ().Max (r => Math.Max (r.GetColumns (), 1));
 			}
 			return max;
 		}
@@ -702,23 +701,23 @@ namespace Terminal.Gui {
 		/// <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)
+		public static int GetSumMaxCharWidth (string 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);
+				max += Math.Max (runes [i].GetColumns (), 1);
 			}
 			return max;
 		}
 
 		/// <summary>
-		/// Gets the number of the Runes in a <see cref="ustring"/> that will fit in <paramref name="columns"/>.
+		/// Gets the number of the Runes in a <see cref="string"/> that will fit in <paramref name="columns"/>.
 		/// </summary>
 		/// <param name="text">The text.</param>
 		/// <param name="columns">The width.</param>
 		/// <returns>The index of the text that fit the width.</returns>
-		public static int GetLengthThatFits (ustring text, int columns) => GetLengthThatFits (text?.ToRuneList (), columns);
+		public static int GetLengthThatFits (string text, int columns) => GetLengthThatFits (text?.ToRuneList (), columns);
 
 		/// <summary>
 		/// Gets the number of the Runes in a list of Runes that will fit in <paramref name="columns"/>.
@@ -735,7 +734,7 @@ namespace Terminal.Gui {
 			var runesLength = 0;
 			var runeIdx = 0;
 			for (; runeIdx < runes.Count; runeIdx++) {
-				var runeWidth = Math.Max (Rune.ColumnWidth (runes [runeIdx]), 1);
+				var runeWidth = Math.Max (runes [runeIdx].GetColumns (), 1);
 				if (runesLength + runeWidth > columns) {
 					break;
 				}
@@ -750,14 +749,14 @@ namespace Terminal.Gui {
 		/// <param name="lines">The lines.</param>
 		/// <param name="width">The width.</param>
 		/// <returns>The index of the list that fit the width.</returns>
-		public static int GetMaxColsForWidth (List<ustring> lines, int width)
+		public static int GetMaxColsForWidth (List<string> lines, int width)
 		{
 			var runesLength = 0;
 			var lineIdx = 0;
 			for (; lineIdx < lines.Count; lineIdx++) {
 				var runes = lines [lineIdx].ToRuneList ();
 				var maxRruneWidth = runes.Count > 0
-					? runes.Max (r => Math.Max (Rune.ColumnWidth (r), 1)) : 1;
+					? runes.Max (r => Math.Max (r.GetColumns (), 1)) : 1;
 				if (runesLength + maxRruneWidth > width) {
 					break;
 				}
@@ -774,9 +773,9 @@ namespace Terminal.Gui {
 		/// <param name="text">The text to measure</param>
 		/// <param name="direction">The text direction.</param>
 		/// <returns></returns>
-		public static Rect CalcRect (int x, int y, ustring text, TextDirection direction = TextDirection.LeftRight_TopBottom)
+		public static Rect CalcRect (int x, int y, string text, TextDirection direction = TextDirection.LeftRight_TopBottom)
 		{
-			if (ustring.IsNullOrEmpty (text)) {
+			if (string.IsNullOrEmpty (text)) {
 				return new Rect (new Point (x, y), Size.Empty);
 			}
 
@@ -787,16 +786,16 @@ namespace Terminal.Gui {
 				int ml = 1;
 
 				int cols = 0;
-				foreach (var rune in text) {
-					if (rune == '\n') {
+				foreach (var rune in text.EnumerateRunes ()) {
+					if (rune.Value == '\n') {
 						ml++;
 						if (cols > mw) {
 							mw = cols;
 						}
 						cols = 0;
-					} else if (rune != '\r') {
+					} else if (rune.Value != '\r') {
 						cols++;
-						var rw = Rune.ColumnWidth (rune);
+						var rw = ((Rune)rune).GetColumns ();
 						if (rw > 0) {
 							rw--;
 						}
@@ -813,17 +812,17 @@ namespace Terminal.Gui {
 				int vh = 0;
 
 				int rows = 0;
-				foreach (var rune in text) {
-					if (rune == '\n') {
+				foreach (var rune in text.EnumerateRunes ()) {
+					if (rune.Value == '\n') {
 						vw++;
 						if (rows > vh) {
 							vh = rows;
 						}
 						rows = 0;
 						cw = 1;
-					} else if (rune != '\r') {
+					} else if (rune.Value != '\r') {
 						rows++;
-						var rw = Rune.ColumnWidth (rune);
+						var rw = ((Rune)rune).GetColumns ();
 						if (cw < rw) {
 							cw = rw;
 							vw++;
@@ -850,9 +849,9 @@ namespace Terminal.Gui {
 		/// <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 (string text, Rune hotKeySpecifier, bool firstUpperCase, out int hotPos, out Key hotKey)
 		{
-			if (ustring.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF) {
+			if (string.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF) {
 				hotPos = -1;
 				hotKey = Key.Unknown;
 				return false;
@@ -865,8 +864,8 @@ namespace Terminal.Gui {
 			// TODO: Ignore hot_key of two are provided
 			// TODO: Do not support non-alphanumeric chars that can't be typed
 			int i = 0;
-			foreach (Rune c in text) {
-				if ((char)c != 0xFFFD) {
+			foreach (Rune c in text.EnumerateRunes ()) {
+				if ((char)c.Value != 0xFFFD) {
 					if (c == hotKeySpecifier) {
 						hot_pos = i;
 					} else if (hot_pos > -1) {
@@ -880,8 +879,8 @@ namespace Terminal.Gui {
 			// Legacy support - use first upper case char if the specifier was not found
 			if (hot_pos == -1 && firstUpperCase) {
 				i = 0;
-				foreach (Rune c in text) {
-					if ((char)c != 0xFFFD) {
+				foreach (Rune c in text.EnumerateRunes ()) {
+					if ((char)c.Value != 0xFFFD) {
 						if (Rune.IsUpper (c)) {
 							hot_key = c;
 							hot_pos = i;
@@ -895,8 +894,8 @@ namespace Terminal.Gui {
 			if (hot_key != (Rune)0 && hot_pos != -1) {
 				hotPos = hot_pos;
 
-				if (hot_key.IsValid && char.IsLetterOrDigit ((char)hot_key)) {
-					hotKey = (Key)char.ToUpperInvariant ((char)hot_key);
+				if (Rune.IsValid (hot_key.Value) && char.IsLetterOrDigit ((char)hot_key.Value)) {
+					hotKey = (Key)char.ToUpperInvariant ((char)hot_key.Value);
 					return true;
 				}
 			}
@@ -916,14 +915,14 @@ namespace Terminal.Gui {
 		/// <remarks>
 		/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for 
 		/// </remarks>
-		public ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
+		public string ReplaceHotKeyWithTag (string text, int hotPos)
 		{
 			// Set the high bit
 			var runes = text.ToRuneList ();
-			if (Rune.IsLetterOrNumber (runes [hotPos])) {
-				runes [hotPos] = new Rune ((uint)runes [hotPos]);
+			if (Rune.IsLetterOrDigit (runes [hotPos])) {
+				runes [hotPos] = new Rune ((uint)runes [hotPos].Value);
 			}
-			return ustring.Make (runes);
+			return StringExtensions.ToString (runes);
 		}
 
 		/// <summary>
@@ -933,33 +932,32 @@ namespace Terminal.Gui {
 		/// <param name="hotKeySpecifier">The hot-key specifier (e.g. '_') to look for.</param>
 		/// <param name="hotPos">Returns the position of the hot-key in the text. -1 if not found.</param>
 		/// <returns>The input text with the hotkey specifier ('_') removed.</returns>
-		public static ustring RemoveHotKeySpecifier (ustring text, int hotPos, Rune hotKeySpecifier)
+		public static string RemoveHotKeySpecifier (string text, int hotPos, Rune hotKeySpecifier)
 		{
-			if (ustring.IsNullOrEmpty (text)) {
+			if (string.IsNullOrEmpty (text)) {
 				return text;
 			}
 
 			// Scan 
-			ustring start = ustring.Empty;
+			string start = string.Empty;
 			int i = 0;
 			foreach (Rune c in text) {
 				if (c == hotKeySpecifier && i == hotPos) {
 					i++;
 					continue;
 				}
-				start += ustring.Make (c);
+				start += c;
 				i++;
 			}
 			return start;
 		}
 		#endregion // Static Members
 
-		List<ustring> _lines = new List<ustring> ();
-		ustring _text;
+		List<string> _lines = new List<string> ();
+		string _text;
 		TextAlignment _textAlignment;
 		VerticalTextAlignment _textVerticalAlignment;
 		TextDirection _textDirection;
-		Attribute _textColor = -1;
 		Key _hotKey;
 		int _hotKeyPos = -1;
 		Size _size;
@@ -970,14 +968,14 @@ namespace Terminal.Gui {
 		public event EventHandler<KeyChangedEventArgs> HotKeyChanged;
 
 		/// <summary>
-		///   The text to be displayed. This text is never modified.
+		///   The text to be displayed. This string is never modified.
 		/// </summary>
-		public virtual ustring Text {
+		public virtual string Text {
 			get => _text;
 			set {
 				_text = value;
 
-				if (_text != null && _text.RuneCount > 0 && (Size.Width == 0 || Size.Height == 0 || Size.Width != _text.ConsoleWidth)) {
+				if (_text != null && _text.GetRuneCount () > 0 && (Size.Width == 0 || Size.Height == 0 || Size.Width != _text.GetColumns ())) {
 					// Provide a default size (width = length of longest line, height = 1)
 					// TODO: It might makes more sense for the default to be width = length of first line?
 					Size = new Size (TextFormatter.MaxWidth (Text, int.MaxValue), 1);
@@ -1160,7 +1158,7 @@ namespace Terminal.Gui {
 		public Size GetFormattedSize ()
 		{
 			var lines = Lines;
-			var width = Lines.Max (line => TextFormatter.GetTextWidth (line));
+			var width = Lines.Max (line => line.GetColumns ());
 			var height = Lines.Count;
 			return new Size (width, height);
 		}
@@ -1171,15 +1169,15 @@ 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, TextDirection)"/> will be called internally. 
+		/// <see cref="Format(string, int, bool, bool, bool, int, TextDirection)"/> will be called internally. 
 		/// </para>
 		/// </remarks>
-		public List<ustring> Lines {
+		public List<string> Lines {
 			get {
 				// With this check, we protect against subclasses with overrides of Text
-				if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
-					_lines = new List<ustring> {
-						ustring.Empty
+				if (string.IsNullOrEmpty (Text) || Size.IsEmpty) {
+					_lines = new List<string> {
+						string.Empty
 					};
 					NeedsFormat = false;
 					return _lines;
@@ -1253,7 +1251,7 @@ namespace Terminal.Gui {
 		public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor, Rect containerBounds = default, bool fillRemaining = true)
 		{
 			// With this check, we protect against subclasses with overrides of Text (like Button)
-			if (ustring.IsNullOrEmpty (_text)) {
+			if (string.IsNullOrEmpty (_text)) {
 				return;
 			}
 
@@ -1322,7 +1320,7 @@ namespace Terminal.Gui {
 						x = bounds.Right - runesWidth;
 						CursorPosition = bounds.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
 					} else {
-						var runesWidth = GetTextWidth (ustring.Make (runes));
+						var runesWidth = StringExtensions.ToString (runes).GetColumns ();
 						x = bounds.Right - runesWidth;
 						CursorPosition = bounds.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0);
 					}
@@ -1340,7 +1338,7 @@ namespace Terminal.Gui {
 						x = bounds.Left + line + ((bounds.Width - runesWidth) / 2);
 						CursorPosition = (bounds.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
 					} else {
-						var runesWidth = GetTextWidth (ustring.Make (runes));
+						var runesWidth = StringExtensions.ToString (runes).GetColumns ();
 						x = bounds.Left + (bounds.Width - runesWidth) / 2;
 						CursorPosition = (bounds.Width - runesWidth) / 2 + (_hotKeyPos > -1 ? _hotKeyPos : 0);
 					}
@@ -1413,13 +1411,13 @@ namespace Terminal.Gui {
 					} else {
 						Application.Driver?.AddRune (rune);
 					}
-					var runeWidth = Math.Max (Rune.ColumnWidth (rune), 1);
+					var runeWidth = Math.Max (rune.GetColumns (), 1);
 					if (isVertical) {
 						current++;
 					} else {
 						current += runeWidth;
 					}
-					var nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length ? Rune.ColumnWidth (runes [idx + 1]) : 0;
+					var nextRuneWidth = idx + 1 > -1 && idx + 1 < runes.Length ? runes [idx + 1].GetColumns () : 0;
 					if (!isVertical && idx + 1 < runes.Length && current + nextRuneWidth > start + size) {
 						break;
 					}

+ 23 - 0
Terminal.Gui/Timeout.cs

@@ -0,0 +1,23 @@
+//
+// MainLoop.cs: IMainLoopDriver and MainLoop for Terminal.Gui
+//
+// Authors:
+//   Miguel de Icaza ([email protected])
+//
+using System;
+
+namespace Terminal.Gui; 
+
+/// <summary>
+/// Provides data for timers running manipulation.
+/// </summary>
+public sealed class Timeout {
+	/// <summary>
+	/// Time to wait before invoke the callback.
+	/// </summary>
+	public TimeSpan Span;
+	/// <summary>
+	/// The function that will be invoked.
+	/// </summary>
+	public Func<MainLoop, bool> Callback;
+}

+ 6 - 9
Terminal.Gui/Types/Rect.cs

@@ -9,15 +9,12 @@
 //
 
 using System;
-using System.Drawing;
 
-namespace Terminal.Gui
-{
+namespace Terminal.Gui {
 	/// <summary>
 	/// Stores a set of four integers that represent the location and size of a rectangle
 	/// </summary>
-	public struct Rect
-	{
+	public struct Rect {
 		int width;
 		int height;
 
@@ -199,7 +196,7 @@ namespace Terminal.Gui
 
 		public static bool operator == (Rect left, Rect right)
 		{
-			return ((left.Location == right.Location) && 
+			return ((left.Location == right.Location) &&
 				(left.Size == right.Size));
 		}
 
@@ -215,7 +212,7 @@ namespace Terminal.Gui
 
 		public static bool operator != (Rect left, Rect right)
 		{
-			return ((left.Location != right.Location) || 
+			return ((left.Location != right.Location) ||
 				(left.Size != right.Size));
 		}
 
@@ -379,7 +376,7 @@ namespace Terminal.Gui
 
 		public bool Contains (int x, int y)
 		{
-			return ((x >= Left) && (x < Right) && 
+			return ((x >= Left) && (x < Right) &&
 				(y >= Top) && (y < Bottom));
 		}
 
@@ -423,7 +420,7 @@ namespace Terminal.Gui
 			if (!(obj is Rect))
 				return false;
 
-			return (this == (Rect) obj);
+			return (this == (Rect)obj);
 		}
 
 		/// <summary>

+ 81 - 52
Terminal.Gui/View/Frame.cs

@@ -1,4 +1,4 @@
-using NStack;
+using System.Text;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -74,6 +74,12 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public override bool OnDrawFrames () => false;
 
+		/// <summary>
+		/// Does nothing for Frame
+		/// </summary>
+		/// <returns></returns>
+		public override bool OnRenderLineCanvas () => false;
+
 		/// <summary>
 		/// Frames only render to their Parent or Parent's SuperView's LineCanvas,
 		/// so this always throws an <see cref="InvalidOperationException"/>.
@@ -118,15 +124,18 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Redraws the Frames that comprise the <see cref="Frame"/>.
 		/// </summary>
-		/// <param name="bounds"></param>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
 			if (Thickness == Thickness.Empty) return;
 
 			if (ColorScheme != null) {
-				Driver.SetAttribute (ColorScheme.Normal);
+				Driver.SetAttribute (GetNormalColor ());
 			} else {
-				Driver.SetAttribute (Parent.GetNormalColor ());
+				if (Id == "Padding") {
+					Driver.SetAttribute (new Attribute (Parent.ColorScheme.HotNormal.Background, Parent.ColorScheme.HotNormal.Foreground));
+				} else {
+					Driver.SetAttribute (Parent.GetNormalColor ());
+				}
 			}
 
 			//Driver.SetAttribute (Colors.Error.Normal);
@@ -154,43 +163,51 @@ namespace Terminal.Gui {
 			var topTitleLineY = borderBounds.Y;
 			var titleY = borderBounds.Y;
 			var titleBarsLength = 0; // the little vertical thingies
-			var maxTitleWidth = Math.Min (Parent.Title.ConsoleWidth, screenBounds.Width - 4);
+			var maxTitleWidth = Math.Min (Parent.Title.GetColumns (), Math.Min (screenBounds.Width - 4, borderBounds.Width - 4));
 			var sideLineLength = borderBounds.Height;
+			var canDrawBorder = borderBounds.Width > 0 && borderBounds.Height > 0;
 
-			if (Thickness.Top == 2) {
-				topTitleLineY = borderBounds.Y - 1;
-				titleY = topTitleLineY + 1;
-				titleBarsLength = 2;
-			}
+			if (!string.IsNullOrEmpty (Parent?.Title)) {
+				if (Thickness.Top == 2) {
+					topTitleLineY = borderBounds.Y - 1;
+					titleY = topTitleLineY + 1;
+					titleBarsLength = 2;
+				}
 
-			// ┌────┐
-			//┌┘View└
-			//│
-			if (Thickness.Top == 3) {
-				topTitleLineY = borderBounds.Y - (Thickness.Top - 1);
-				titleY = topTitleLineY + 1;
-				titleBarsLength = 3;
-				sideLineLength++;
-			}
+				// ┌────┐
+				//┌┘View└
+				//│
+				if (Thickness.Top == 3) {
+					topTitleLineY = borderBounds.Y - (Thickness.Top - 1);
+					titleY = topTitleLineY + 1;
+					titleBarsLength = 3;
+					sideLineLength++;
+				}
+
+				// ┌────┐
+				//┌┘View└
+				//│
+				if (Thickness.Top > 3) {
+					topTitleLineY = borderBounds.Y - 2;
+					titleY = topTitleLineY + 1;
+					titleBarsLength = 3;
+					sideLineLength++;
+				}
 
-			// ┌────┐
-			//┌┘View└
-			//│
-			if (Thickness.Top > 3) {
-				topTitleLineY = borderBounds.Y - 2;
-				titleY = topTitleLineY + 1;
-				titleBarsLength = 3;
-				sideLineLength++;
 			}
 
-			if (Id == "Border" && Thickness.Top > 0 && maxTitleWidth > 0 && !ustring.IsNullOrEmpty (Parent?.Title)) {
+			if (Id == "Border" && canDrawBorder && Thickness.Top > 0 && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) {
 				var prevAttr = Driver.GetAttribute ();
-				Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ());
-				DrawTitle (new Rect (borderBounds.X, titleY, Math.Min (borderBounds.Width - 4, Parent.Title.ConsoleWidth), 1), Parent?.Title);
+				if (ColorScheme != null) {
+					Driver.SetAttribute (HasFocus ? GetHotNormalColor () : GetNormalColor ());
+				} else {
+					Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ());
+				}
+				DrawTitle (new Rect (borderBounds.X, titleY, maxTitleWidth, 1), Parent?.Title);
 				Driver.SetAttribute (prevAttr);
 			}
 
-			if (Id == "Border" && BorderStyle != LineStyle.None) {
+			if (Id == "Border" && canDrawBorder && BorderStyle != LineStyle.None) {
 				LineCanvas lc = Parent?.LineCanvas;
 
 				var drawTop = Thickness.Top > 0 && Frame.Width > 1 && Frame.Height > 1;
@@ -198,48 +215,56 @@ namespace Terminal.Gui {
 				var drawBottom = Thickness.Bottom > 0 && Frame.Width > 1;
 				var drawRight = Thickness.Right > 0 && (Frame.Height > 1 || Thickness.Top == 0);
 
+				var prevAttr = Driver.GetAttribute ();
+				if (ColorScheme != null) {
+					Driver.SetAttribute (GetNormalColor ());
+				} else {
+					Driver.SetAttribute (Parent.GetNormalColor ());
+				}
+
 				if (drawTop) {
 					// ╔╡Title╞═════╗
 					// ╔╡╞═════╗
-					if (Frame.Width < 4 || ustring.IsNullOrEmpty (Parent?.Title)) {
+					if (borderBounds.Width < 4 || string.IsNullOrEmpty (Parent?.Title)) {
 						// ╔╡╞╗ should be ╔══╗
-						lc.AddLine (new Point (borderBounds.Location.X, titleY), borderBounds.Width, Orientation.Horizontal, BorderStyle);
+						lc.AddLine (new Point (borderBounds.Location.X, titleY), borderBounds.Width, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
 					} else {
 
 						// ┌────┐
 						//┌┘View└
 						//│
 						if (Thickness.Top == 2) {
-							lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle);
+							lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
 						}
 						// ┌────┐
 						//┌┘View└
 						//│
-						if (Thickness.Top > 2) {
-							lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle);
-							lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY + 2), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle);
+						if (borderBounds.Width >= 4 && Thickness.Top > 2) {
+							lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
+							lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY + 2), Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
 						}
 
 						// ╔╡Title╞═════╗
 						// Add a short horiz line for ╔╡
-						lc.AddLine (new Point (borderBounds.Location.X, titleY), 2, Orientation.Horizontal, BorderStyle);
+						lc.AddLine (new Point (borderBounds.Location.X, titleY), 2, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
 						// Add a vert line for ╔╡
-						lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single);
+						lc.AddLine (new Point (borderBounds.X + 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, Driver.GetAttribute ());
 						// Add a vert line for ╞
-						lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single);
+						lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, topTitleLineY), titleBarsLength, Orientation.Vertical, LineStyle.Single, Driver.GetAttribute ());
 						// Add the right hand line for ╞═════╗
-						lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, titleY), borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle);
+						lc.AddLine (new Point (borderBounds.X + 1 + Math.Min (borderBounds.Width - 2, maxTitleWidth + 2) - 1, titleY), borderBounds.Width - Math.Min (borderBounds.Width - 2, maxTitleWidth + 2), Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
 					}
 				}
 				if (drawLeft) {
-					lc.AddLine (new Point (borderBounds.Location.X, titleY), sideLineLength, Orientation.Vertical, BorderStyle);
+					lc.AddLine (new Point (borderBounds.Location.X, titleY), sideLineLength, Orientation.Vertical, BorderStyle, Driver.GetAttribute ());
 				}
 				if (drawBottom) {
-					lc.AddLine (new Point (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), borderBounds.Width, Orientation.Horizontal, BorderStyle);
+					lc.AddLine (new Point (borderBounds.X, borderBounds.Y + borderBounds.Height - 1), borderBounds.Width, Orientation.Horizontal, BorderStyle, Driver.GetAttribute ());
 				}
 				if (drawRight) {
-					lc.AddLine (new Point (borderBounds.X + borderBounds.Width - 1, titleY), sideLineLength, Orientation.Vertical, BorderStyle);
+					lc.AddLine (new Point (borderBounds.X + borderBounds.Width - 1, titleY), sideLineLength, Orientation.Vertical, BorderStyle, Driver.GetAttribute ());
 				}
+				Driver.SetAttribute (prevAttr);
 
 				// TODO: This should be moved to LineCanvas as a new BorderStyle.Ruler
 				if ((ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler) {
@@ -250,10 +275,14 @@ namespace Terminal.Gui {
 					}
 
 					// Redraw title 
-					if (drawTop && Id == "Border" && maxTitleWidth > 0 && !ustring.IsNullOrEmpty (Parent?.Title)) {
-						var prevAttr = Driver.GetAttribute ();
-						Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ());
-						DrawTitle (new Rect (borderBounds.X, titleY, Parent.Title.ConsoleWidth, 1), Parent?.Title);
+					if (drawTop && Id == "Border" && maxTitleWidth > 0 && !string.IsNullOrEmpty (Parent?.Title)) {
+						prevAttr = Driver.GetAttribute ();
+						if (ColorScheme != null) {
+							Driver.SetAttribute (HasFocus ? GetHotNormalColor () : GetNormalColor ());
+						} else {
+							Driver.SetAttribute (Parent.HasFocus ? Parent.GetHotNormalColor () : Parent.GetNormalColor ());
+						}
+						DrawTitle (new Rect (borderBounds.X, titleY, Parent.Title.GetColumns (), 1), Parent?.Title);
 						Driver.SetAttribute (prevAttr);
 					}
 
@@ -332,13 +361,13 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="region">Screen relative region where the title will be drawn.</param>
 		/// <param name="title">The title.</param>
-		public void DrawTitle (Rect region, ustring title)
+		public void DrawTitle (Rect region, string title)
 		{
 			var width = region.Width;
-			if (!ustring.IsNullOrEmpty (title)) {
+			if (!string.IsNullOrEmpty (title)) {
 				Driver.Move (region.X + 2, region.Y);
 				//Driver.AddRune (' ');
-				var str = title.Sum (r => Math.Max (Rune.ColumnWidth (r), 1)) >= width
+				var str = title.EnumerateRunes ().Sum (r => Math.Max (r.GetColumns (), 1)) >= width
 					? TextFormatter.Format (title, width, false, false) [0] : title;
 				Driver.AddStr (str);
 			}

+ 1 - 0
Terminal.Gui/View/Layout/PosDim.cs

@@ -345,6 +345,7 @@ namespace Terminal.Gui {
 				case 3: tside = "bottom"; break;
 				default: tside = "unknown"; break;
 				}
+				// Note: We do not checkt `Target` for null here to intentionally throw if so
 				return $"View({tside},{Target.ToString ()})";
 			}
 

+ 4 - 4
Terminal.Gui/View/TitleEventArgs.cs

@@ -1,5 +1,5 @@
 using System;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -9,12 +9,12 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The new Window Title.
 		/// </summary>
-		public ustring NewTitle { get; set; }
+		public string NewTitle { get; set; }
 
 		/// <summary>
 		/// The old Window Title.
 		/// </summary>
-		public ustring OldTitle { get; set; }
+		public string OldTitle { get; set; }
 
 		/// <summary>
 		/// Flag which allows canceling the Title change.
@@ -26,7 +26,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="oldTitle">The <see cref="View.Title"/> that is/has been replaced.</param>
 		/// <param name="newTitle">The new <see cref="View.Title"/> to be replaced.</param>
-		public TitleEventArgs (ustring oldTitle, ustring newTitle)
+		public TitleEventArgs (string oldTitle, string newTitle)
 		{
 			OldTitle = oldTitle;
 			NewTitle = newTitle;

+ 12 - 12
Terminal.Gui/View/View.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	#region API Docs
@@ -163,7 +163,7 @@ namespace Terminal.Gui {
 		/// <param name="x">column to locate the View.</param>
 		/// <param name="y">row to locate the View.</param>
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
-		public View (int x, int y, ustring text) : this (TextFormatter.CalcRect (x, y, text), text) { }
+		public View (int x, int y, string text) : this (TextFormatter.CalcRect (x, y, text), text) { }
 
 		/// <summary>
 		///   Initializes a new instance of <see cref="View"/> using <see cref="Terminal.Gui.LayoutStyle.Absolute"/> layout.
@@ -180,7 +180,7 @@ namespace Terminal.Gui {
 		/// </remarks>
 		/// <param name="rect">Location.</param>
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
-		public View (Rect rect, ustring text)
+		public View (Rect rect, string text)
 		{
 			SetInitialProperties (text, rect, LayoutStyle.Absolute, TextDirection.LeftRight_TopBottom);
 		}
@@ -200,7 +200,7 @@ namespace Terminal.Gui {
 		/// </remarks>
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
 		/// <param name="direction">The text direction.</param>
-		public View (ustring text, TextDirection direction = TextDirection.LeftRight_TopBottom)
+		public View (string text, TextDirection direction = TextDirection.LeftRight_TopBottom)
 		{
 			SetInitialProperties (text, Rect.Empty, LayoutStyle.Computed, direction);
 		}
@@ -213,7 +213,7 @@ namespace Terminal.Gui {
 		/// <param name="rect"></param>
 		/// <param name="layoutStyle"></param>
 		/// <param name="direction"></param>
-		void SetInitialProperties (ustring text, Rect rect, LayoutStyle layoutStyle = LayoutStyle.Computed,
+		void SetInitialProperties (string text, Rect rect, LayoutStyle layoutStyle = LayoutStyle.Computed,
 		    TextDirection direction = TextDirection.LeftRight_TopBottom)
 		{
 			TextFormatter = new TextFormatter ();
@@ -226,7 +226,7 @@ namespace Terminal.Gui {
 			TabStop = false;
 			LayoutStyle = layoutStyle;
 
-			Text = text == null ? ustring.Empty : text;
+			Text = text == null ? string.Empty : text;
 			LayoutStyle = layoutStyle;
 			Frame = rect.IsEmpty ? TextFormatter.CalcRect (0, 0, text, direction) : rect;
 			OnResizeNeeded ();
@@ -334,13 +334,13 @@ namespace Terminal.Gui {
 		/// <remarks>The id should be unique across all Views that share a SuperView.</remarks>
 		public string Id { get; set; } = "";
 
-		ustring _title = ustring.Empty;
+		string _title = string.Empty;
 		/// <summary>
 		/// The title to be displayed for this <see cref="View"/>. The title will be displayed if <see cref="Border"/>.<see cref="Thickness.Top"/>
 		/// is greater than 0.
 		/// </summary>
 		/// <value>The title.</value>
-		public ustring Title {
+		public string Title {
 			get => _title;
 			set {
 				if (!OnTitleChanging (_title, value)) {
@@ -363,7 +363,7 @@ namespace Terminal.Gui {
 		/// <param name="oldTitle">The <see cref="View.Title"/> that is/has been replaced.</param>
 		/// <param name="newTitle">The new <see cref="View.Title"/> to be replaced.</param>
 		/// <returns>`true` if an event handler canceled the Title change.</returns>
-		public virtual bool OnTitleChanging (ustring oldTitle, ustring newTitle)
+		public virtual bool OnTitleChanging (string oldTitle, string newTitle)
 		{
 			var args = new TitleEventArgs (oldTitle, newTitle);
 			TitleChanging?.Invoke (this, args);
@@ -381,7 +381,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="oldTitle">The <see cref="View.Title"/> that is/has been replaced.</param>
 		/// <param name="newTitle">The new <see cref="View.Title"/> to be replaced.</param>
-		public virtual void OnTitleChanged (ustring oldTitle, ustring newTitle)
+		public virtual void OnTitleChanged (string oldTitle, string newTitle)
 		{
 			var args = new TitleEventArgs (oldTitle, newTitle);
 			TitleChanged?.Invoke (this, args);
@@ -434,7 +434,7 @@ namespace Terminal.Gui {
 				}
 			}
 		}
-		
+
 		/// <summary>
 		/// Event fired when the <see cref="Visible"/> value is being changed.
 		/// </summary>
@@ -481,7 +481,7 @@ namespace Terminal.Gui {
 
 			return true;
 		}
-		
+
 		/// <summary>
 		/// Pretty prints the View
 		/// </summary>

+ 77 - 64
Terminal.Gui/View/ViewDrawing.cs

@@ -1,6 +1,6 @@
 using System;
 using System.Linq;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	public partial class View {
@@ -76,12 +76,12 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Removes the <see cref="_needsDisplay"/> and the <see cref="_childNeedsDisplay"/> setting on this view.
+		/// Removes the <see cref="_needsDisplay"/> and the <see cref="_subViewNeedsDisplay"/> setting on this view.
 		/// </summary>
 		protected void ClearNeedsDisplay ()
 		{
 			_needsDisplay = Rect.Empty;
-			_childNeedsDisplay = false;
+			_subViewNeedsDisplay = false;
 		}
 
 		// The view-relative region that needs to be redrawn
@@ -102,9 +102,9 @@ namespace Terminal.Gui {
 		/// <param name="region">The view-relative region that needs to be redrawn.</param>
 		public void SetNeedsDisplay (Rect region)
 		{
-			if (_needsDisplay.IsEmpty)
+			if (_needsDisplay.IsEmpty) {
 				_needsDisplay = region;
-			else {
+			} else {
 				var x = Math.Min (_needsDisplay.X, region.X);
 				var y = Math.Min (_needsDisplay.Y, region.Y);
 				var w = Math.Max (_needsDisplay.Width, region.Width);
@@ -125,18 +125,18 @@ namespace Terminal.Gui {
 				}
 		}
 
-		internal bool _childNeedsDisplay { get; private set; }
+		internal bool _subViewNeedsDisplay { get; private set; }
 
 		/// <summary>
 		/// Indicates that any Subviews (in the <see cref="Subviews"/> list) need to be repainted.
 		/// </summary>
 		public void SetSubViewNeedsDisplay ()
 		{
-			if (_childNeedsDisplay) {
+			if (_subViewNeedsDisplay) {
 				return;
 			}
-			_childNeedsDisplay = true;
-			if (_superView != null && !_superView._childNeedsDisplay)
+			_subViewNeedsDisplay = true;
+			if (_superView != null && !_superView._subViewNeedsDisplay)
 				_superView.SetSubViewNeedsDisplay ();
 		}
 
@@ -155,7 +155,7 @@ namespace Terminal.Gui {
 			for (var line = 0; line < h; line++) {
 				Move (0, line);
 				for (var col = 0; col < w; col++)
-					Driver.AddRune (' ');
+					Driver.AddRune ((Rune)' ');
 			}
 		}
 
@@ -174,7 +174,7 @@ namespace Terminal.Gui {
 			for (var line = regionScreen.Y; line < regionScreen.Y + h; line++) {
 				Driver.Move (regionScreen.X, line);
 				for (var col = 0; col < w; col++)
-					Driver.AddRune (' ');
+					Driver.AddRune ((Rune)' ');
 			}
 		}
 
@@ -203,9 +203,7 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public Rect ClipToBounds ()
 		{
-			var clip = Bounds;
-
-			return SetClip (clip);
+			return SetClip (Bounds);
 		}
 
 		// BUGBUG: v2 - SetClip should return VIEW-relative so that it can be used to reset it; using Driver.Clip directly should not be necessary. 
@@ -234,16 +232,16 @@ namespace Terminal.Gui {
 		/// <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)
+		public void DrawHotString (string text, Attribute hotColor, Attribute normalColor)
 		{
 			var hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier;
 			Application.Driver.SetAttribute (normalColor);
 			foreach (var rune in text) {
-				if (rune == hotkeySpec) {
+				if (rune == hotkeySpec.Value) {
 					Application.Driver.SetAttribute (hotColor);
 					continue;
 				}
-				Application.Driver.AddRune (rune);
+				Application.Driver.AddRune ((Rune)rune);
 				Application.Driver.SetAttribute (normalColor);
 			}
 		}
@@ -254,7 +252,7 @@ namespace Terminal.Gui {
 		/// <param name="text">String to display, the underscore before a letter flags the next letter as the hotkey.</param>
 		/// <param name="focused">If set to <see langword="true"/> this uses the focused colors from the color scheme, otherwise the regular ones.</param>
 		/// <param name="scheme">The color scheme to use.</param>
-		public void DrawHotString (ustring text, bool focused, ColorScheme scheme)
+		public void DrawHotString (string text, bool focused, ColorScheme scheme)
 		{
 			if (focused)
 				DrawHotString (text, scheme.HotFocus, scheme.Focus);
@@ -295,7 +293,9 @@ namespace Terminal.Gui {
 
 		// TODO: Make this cancelable
 		/// <summary>
-		/// 
+		/// Prepares <see cref="View.LineCanvas"/>. If <see cref="SuperViewRendersLineCanvas"/> is true, only the <see cref="LineCanvas"/> of 
+		/// this view's subviews will be rendered. If <see cref="SuperViewRendersLineCanvas"/> is false (the default), this 
+		/// method will cause the <see cref="LineCanvas"/> be prepared to be rendered.
 		/// </summary>
 		/// <returns></returns>
 		public virtual bool OnDrawFrames ()
@@ -315,9 +315,9 @@ namespace Terminal.Gui {
 
 			// Each of these renders lines to either this View's LineCanvas 
 			// Those lines will be finally rendered in OnRenderLineCanvas
-			Margin?.Redraw (Margin.Frame);
-			Border?.Redraw (Border.Frame);
-			Padding?.Redraw (Padding.Frame);
+			Margin?.OnDrawContent (Bounds);
+			Border?.OnDrawContent (Bounds);
+			Padding?.OnDrawContent (Bounds);
 
 			Driver.Clip = prevClip;
 
@@ -325,23 +325,23 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Redraws this view and its subviews; only redraws the views that have been flagged for a re-display.
+		/// Draws the view. Causes the following virtual methods to be called (along with their related events): 
+		/// <see cref="OnDrawContent"/>, <see cref="OnDrawContentComplete"/>.
 		/// </summary>
-		/// <param name="bounds">The bounds (view-relative region) to redraw.</param>
 		/// <remarks>
 		/// <para>
-		///    Always use <see cref="Bounds"/> (view-relative) when calling <see cref="Redraw(Rect)"/>, NOT <see cref="Frame"/> (superview-relative).
+		///    Always use <see cref="Bounds"/> (view-relative) when calling <see cref="OnDrawContent(Rect)"/>, NOT <see cref="Frame"/> (superview-relative).
 		/// </para>
 		/// <para>
 		///    Views should set the color that they want to use on entry, as otherwise this will inherit
 		///    the last color that was set globally on the driver.
 		/// </para>
 		/// <para>
-		///    Overrides of <see cref="Redraw"/> must ensure they do not set <c>Driver.Clip</c> to a clip region
-		///    larger than the <ref name="bounds"/> parameter, as this will cause the driver to clip the entire region.
+		///    Overrides of <see cref="OnDrawContent(Rect)"/> must ensure they do not set <c>Driver.Clip</c> to a clip region
+		///    larger than the <ref name="Bounds"/> property, as this will cause the driver to clip the entire region.
 		/// </para>
 		/// </remarks>
-		public virtual void Redraw (Rect bounds)
+		public void Draw ()
 		{
 			if (!CanBeVisible (this)) {
 				return;
@@ -356,32 +356,12 @@ namespace Terminal.Gui {
 				Driver.SetAttribute (GetNormalColor ());
 			}
 
-			if (SuperView != null) {
-				Clear (ViewToScreen (bounds));
-			}
-
 			// Invoke DrawContentEvent
-			OnDrawContent (bounds);
-
-			// Draw subviews
-			// TODO: Implement OnDrawSubviews (cancelable);
-			if (_subviews != null) {
-				foreach (var view in _subviews) {
-					if (view.Visible) { //!view._needsDisplay.IsEmpty || view._childNeedsDisplay || view.LayoutNeeded) {
-						if (true) { //view.Frame.IntersectsWith (bounds)) { // && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) {
-							if (view.LayoutNeeded) {
-								view.LayoutSubviews ();
-							}
+			var dev = new DrawEventArgs (Bounds);
+			DrawContent?.Invoke (this, dev);
 
-							// Draw the subview
-							// Use the view's bounds (view-relative; Location will always be (0,0)
-							//if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) {
-							view.Redraw (view.Bounds);
-							//}
-						}
-						view.ClearNeedsDisplay ();
-					}
-				}
+			if (!dev.Cancel) {
+				OnDrawContent (Bounds);
 			}
 
 			Driver.Clip = prevClip;
@@ -389,15 +369,26 @@ namespace Terminal.Gui {
 			OnRenderLineCanvas ();
 
 			// Invoke DrawContentCompleteEvent
-			OnDrawContentComplete (bounds);
+			OnDrawContentComplete (Bounds);
 
 			// BUGBUG: v2 - We should be able to use View.SetClip here and not have to resort to knowing Driver details.
 			ClearLayoutNeeded ();
 			ClearNeedsDisplay ();
 		}
 
-		internal void OnRenderLineCanvas ()
+		// TODO: Make this cancelable
+		/// <summary>
+		/// Renders <see cref="View.LineCanvas"/>. If <see cref="SuperViewRendersLineCanvas"/> is true, only the <see cref="LineCanvas"/> of 
+		/// this view's subviews will be rendered. If <see cref="SuperViewRendersLineCanvas"/> is false (the default), this 
+		/// method will cause the <see cref="LineCanvas"/> to be rendered.
+		/// </summary>
+		/// <returns></returns>
+		public virtual bool OnRenderLineCanvas ()
 		{
+			if (!IsInitialized) {
+				return false;
+			}
+
 			//Driver.SetAttribute (new Attribute(Color.White, Color.Black));
 
 			// If we have a SuperView, it'll render our frames.
@@ -410,8 +401,8 @@ namespace Terminal.Gui {
 				LineCanvas.Clear ();
 			}
 
-			if (Subviews.Select (s => s.SuperViewRendersLineCanvas).Count () > 0) {
-				foreach (var subview in Subviews.Where (s => s.SuperViewRendersLineCanvas)) {
+			if (Subviews.Where (s => s.SuperViewRendersLineCanvas).Count () > 0) {
+				foreach (var subview in Subviews.Where (s => s.SuperViewRendersLineCanvas == true)) {
 					// Combine the LineCavas'
 					LineCanvas.Merge (subview.LineCanvas);
 					subview.LineCanvas.Clear ();
@@ -424,6 +415,8 @@ namespace Terminal.Gui {
 				}
 				LineCanvas.Clear ();
 			}
+
+			return true;
 		}
 
 		/// <summary>
@@ -448,20 +441,40 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public virtual void OnDrawContent (Rect contentArea)
 		{
-			// TODO: Make DrawContent a cancelable event
-			// if (!DrawContent?.Invoke(this, new DrawEventArgs (viewport)) {
-			DrawContent?.Invoke (this, new DrawEventArgs (contentArea));
+			if (SuperView != null) {
+				Clear (ViewToScreen (Bounds));
+			}
 
-			if (!ustring.IsNullOrEmpty (TextFormatter.Text)) {
+			if (!string.IsNullOrEmpty (TextFormatter.Text)) {
 				if (TextFormatter != null) {
 					TextFormatter.NeedsFormat = true;
 				}
 				// This should NOT clear 
-				TextFormatter?.Draw (ViewToScreen (contentArea), HasFocus ? GetFocusColor () : GetNormalColor (),
+				TextFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? GetFocusColor () : GetNormalColor (),
 				    HasFocus ? ColorScheme.HotFocus : GetHotNormalColor (),
 				    Rect.Empty, false);
 				SetSubViewNeedsDisplay ();
 			}
+
+			// Draw subviews
+			// TODO: Implement OnDrawSubviews (cancelable);
+			if (_subviews != null) {
+				foreach (var view in _subviews) {
+					if (view.Visible) { //!view._needsDisplay.IsEmpty || view._childNeedsDisplay || view.LayoutNeeded) {
+						if (true) { //view.Frame.IntersectsWith (bounds)) { // && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) {
+							if (view.LayoutNeeded) {
+								view.LayoutSubviews ();
+							}
+
+							// Draw the subview
+							// Use the view's bounds (view-relative; Location will always be (0,0)
+							//if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) {
+							view.Draw ();
+							//}
+						}
+					}
+				}
+			}
 		}
 
 		/// <summary>
@@ -480,13 +493,13 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Enables overrides after completed drawing infinitely scrolled content and/or a background behind removed controls.
 		/// </summary>
-		/// <param name="viewport">The view-relative rectangle describing the currently visible viewport into the <see cref="View"/></param>
+		/// <param name="contentArea">The view-relative rectangle describing the currently visible viewport into the <see cref="View"/></param>
 		/// <remarks>
 		/// This method will be called after any subviews removed with <see cref="Remove(View)"/> have been completed drawing.
 		/// </remarks>
-		public virtual void OnDrawContentComplete (Rect viewport)
+		public virtual void OnDrawContentComplete (Rect contentArea)
 		{
-			DrawContentComplete?.Invoke (this, new DrawEventArgs (viewport));
+			DrawContentComplete?.Invoke (this, new DrawEventArgs (contentArea));
 		}
 
 	}

+ 5 - 0
Terminal.Gui/View/ViewEventArgs.cs

@@ -55,6 +55,11 @@ namespace Terminal.Gui {
 		/// Gets the view-relative rectangle describing the currently visible viewport into the <see cref="View"/>.
 		/// </summary>
 		public Rect Rect { get; }
+
+		/// <summary>
+		/// If set to true, the draw operation will be canceled, if applicable.
+		/// </summary>
+		public bool Cancel { get; set; }
 	}
 
 	/// <summary>

+ 6 - 6
Terminal.Gui/View/ViewKeyboard.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	public partial class View  {
@@ -25,7 +25,7 @@ namespace Terminal.Gui {
 					var v = value == Key.Unknown ? Key.Null : value;
 					if (_hotKey != Key.Null && ContainsKeyBinding (Key.Space | _hotKey)) {
 						if (v == Key.Null) {
-							ClearKeybinding (Key.Space | _hotKey);
+							ClearKeyBinding (Key.Space | _hotKey);
 						} else {
 							ReplaceKeyBinding (Key.Space | _hotKey, Key.Space | v);
 						}
@@ -69,7 +69,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The keystroke combination used in the <see cref="Shortcut"/> as string.
 		/// </summary>
-		public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (_shortcutHelper.Shortcut);
+		public string ShortcutTag => ShortcutHelper.GetShortcutTag (_shortcutHelper.Shortcut);
 
 		/// <summary>
 		/// The action to run if the <see cref="Shortcut"/> is defined.
@@ -273,7 +273,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Removes all bound keys from the View and resets the default bindings.
 		/// </summary>
-		public void ClearKeybindings ()
+		public void ClearKeyBindings ()
 		{
 			KeyBindings.Clear ();
 		}
@@ -282,7 +282,7 @@ namespace Terminal.Gui {
 		/// Clears the existing keybinding (if any) for the given <paramref name="key"/>.
 		/// </summary>
 		/// <param name="key"></param>
-		public void ClearKeybinding (Key key)
+		public void ClearKeyBinding (Key key)
 		{
 			KeyBindings.Remove (key);
 		}
@@ -292,7 +292,7 @@ namespace Terminal.Gui {
 		/// keys bound to the same command and this method will clear all of them.
 		/// </summary>
 		/// <param name="command"></param>
-		public void ClearKeybinding (params Command [] command)
+		public void ClearKeyBinding (params Command [] command)
 		{
 			foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command)).ToArray ()) {
 				KeyBindings.Remove (kvp.Key);

+ 22 - 28
Terminal.Gui/View/ViewLayout.cs

@@ -4,7 +4,7 @@ using System.ComponentModel;
 using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -198,7 +198,7 @@ namespace Terminal.Gui {
 
 		/// <summary>
 		/// The View-relative rectangle where View content is displayed. SubViews are positioned relative to 
-		/// Bounds.<see cref="Rect.Location">Location</see> (which is always (0, 0)) and <see cref="Redraw(Rect)"/> clips drawing to 
+		/// Bounds.<see cref="Rect.Location">Location</see> (which is always (0, 0)) and <see cref="Draw()"/> clips drawing to 
 		/// Bounds.<see cref="Rect.Size">Size</see>.
 		/// </summary>
 		/// <value>The bounds.</value>
@@ -379,12 +379,16 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public bool GetMinimumBounds (out Size size)
 		{
+			if (!IsInitialized) {
+				size = new Size (0, 0);
+				return false;
+			}
 			size = Bounds.Size;
 
-			if (!AutoSize && !ustring.IsNullOrEmpty (TextFormatter.Text)) {
+			if (!AutoSize && !string.IsNullOrEmpty (TextFormatter.Text)) {
 				switch (TextFormatter.IsVerticalDirection (TextDirection)) {
 				case true:
-					var colWidth = TextFormatter.GetSumMaxCharWidth (new List<ustring> { TextFormatter.Text }, 0, 1);
+					var colWidth = TextFormatter.GetSumMaxCharWidth (new List<string> { TextFormatter.Text }, 0, 1);
 					// TODO: v2 - This uses frame.Width; it should only use Bounds
 					if (_frame.Width < colWidth &&
 						(Width == null ||
@@ -469,13 +473,13 @@ namespace Terminal.Gui {
 			if (LayoutNeeded)
 				return;
 			LayoutNeeded = true;
-			if (SuperView == null)
-				return;
-			SuperView.SetNeedsLayout ();
 			foreach (var view in Subviews) {
 				view.SetNeedsLayout ();
 			}
 			TextFormatter.NeedsFormat = true;
+			if (SuperView == null)
+				return;
+			SuperView.SetNeedsLayout ();
 		}
 
 		/// <summary>
@@ -494,28 +498,12 @@ namespace Terminal.Gui {
 		/// <param name="y">Y screen-coordinate point.</param>
 		public Point ScreenToView (int x, int y)
 		{
+			Point boundsOffset = SuperView == null ? Point.Empty : SuperView.GetBoundsOffset ();
 			if (SuperView == null) {
-				return new Point (x - Frame.X, y - _frame.Y);
-			} else {
-				var parent = SuperView.ScreenToView (x, y);
-				return new Point (parent.X - _frame.X, parent.Y - _frame.Y);
-			}
-		}
-
-		/// <summary>
-		/// Converts a point from screen-relative coordinates to bounds-relative coordinates.
-		/// </summary>
-		/// <returns>The mapped point.</returns>
-		/// <param name="x">X screen-coordinate point.</param>
-		/// <param name="y">Y screen-coordinate point.</param>
-		public Point ScreenToBounds (int x, int y)
-		{
-			if (SuperView == null) {
-				var boundsOffset = GetBoundsOffset ();
 				return new Point (x - Frame.X + boundsOffset.X, y - Frame.Y + boundsOffset.Y);
 			} else {
-				var parent = SuperView.ScreenToView (x, y);
-				return new Point (parent.X - _frame.X, parent.Y - _frame.Y);
+				var parent = SuperView.ScreenToView (x - boundsOffset.X, y - boundsOffset.Y);
+				return new Point (parent.X - Frame.X, parent.Y - Frame.Y);
 			}
 		}
 
@@ -962,7 +950,7 @@ namespace Terminal.Gui {
 
 			var aSize = true;
 			var nBoundsSize = GetAutoSize ();
-			if (nBoundsSize != Bounds.Size) {
+			if (IsInitialized && nBoundsSize != Bounds.Size) {
 				if (ForceValidatePosDim) {
 					aSize = SetWidthHeight (nBoundsSize);
 				} else {
@@ -1007,7 +995,13 @@ namespace Terminal.Gui {
 		/// <returns>The <see cref="Size"/> required to fit the text.</returns>
 		public Size GetAutoSize ()
 		{
-			var rect = TextFormatter.CalcRect (Bounds.X, Bounds.Y, TextFormatter.Text, TextFormatter.Direction);
+			int x = 0;
+			int y = 0;
+			if (IsInitialized) {
+				x = Bounds.X;
+				y = Bounds.Y; 
+			}
+			var rect = TextFormatter.CalcRect (x, y,TextFormatter.Text, TextFormatter.Direction);
 			var newWidth = rect.Size.Width - GetHotKeySpecifierLength () + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal;
 			var newHeight = rect.Size.Height - GetHotKeySpecifierLength (false) + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical;
 			return new Size (newWidth, newHeight);

+ 1 - 1
Terminal.Gui/View/ViewMouse.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	public partial class View  {

+ 1 - 1
Terminal.Gui/View/ViewSubViews.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Linq;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	public partial class View  {

+ 9 - 9
Terminal.Gui/View/ViewText.cs

@@ -1,10 +1,10 @@
-using NStack;
+using System.Text;
 using System;
 
 namespace Terminal.Gui {
 
 	public partial class View {
-		ustring _text;
+		string _text;
 
 		/// <summary>
 		///   The text displayed by the <see cref="View"/>.
@@ -26,7 +26,7 @@ namespace Terminal.Gui {
 		///  <c>(Rune)0xffff</c>.
 		/// </para>
 		/// </remarks>
-		public virtual ustring Text {
+		public virtual string Text {
 			get => _text;
 			set {
 				_text = value;
@@ -37,7 +37,7 @@ namespace Terminal.Gui {
 
 #if DEBUG
 				if (_text != null && string.IsNullOrEmpty (Id)) {
-					Id = _text.ToString ();
+					Id = _text;
 				}
 #endif
 			}
@@ -154,12 +154,12 @@ namespace Terminal.Gui {
 		{
 			if (isWidth) {
 				return TextFormatter.IsHorizontalDirection (TextDirection) &&
-				    TextFormatter.Text?.Contains (HotKeySpecifier) == true
-				    ? Math.Max (Rune.ColumnWidth (HotKeySpecifier), 0) : 0;
+				    TextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true
+				    ? Math.Max (HotKeySpecifier.GetColumns (), 0) : 0;
 			} else {
 				return TextFormatter.IsVerticalDirection (TextDirection) &&
-				    TextFormatter.Text?.Contains (HotKeySpecifier) == true
-				    ? Math.Max (Rune.ColumnWidth (HotKeySpecifier), 0) : 0;
+				    TextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true
+				    ? Math.Max (HotKeySpecifier.GetColumns(), 0) : 0;
 			}
 		}
 
@@ -179,7 +179,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public Size GetSizeNeededForTextAndHotKey ()
 		{
-			if (ustring.IsNullOrEmpty (TextFormatter.Text)) {
+			if (string.IsNullOrEmpty (TextFormatter.Text)) {
 
 				if (!IsInitialized) return Size.Empty;
 

+ 10 - 11
Terminal.Gui/Views/AutocompleteFilepathContext.cs

@@ -1,4 +1,4 @@
-using NStack;
+using System.Text;
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -10,8 +10,8 @@ namespace Terminal.Gui {
 	internal class AutocompleteFilepathContext : AutocompleteContext {
 		public FileDialogState State { get; set; }
 
-		public AutocompleteFilepathContext (ustring currentLine, int cursorPosition, FileDialogState state)
-			: base (currentLine.ToRuneList (), cursorPosition)
+		public AutocompleteFilepathContext (string currentLine, int cursorPosition, FileDialogState state)
+			: base (currentLine.EnumerateRunes ().ToList (), cursorPosition)
 		{
 			this.State = state;
 		}
@@ -30,18 +30,17 @@ namespace Terminal.Gui {
 				return Enumerable.Empty<Suggestion> ();
 			}
 
-			var path = ustring.Make (context.CurrentLine).ToString ();
+			var path = StringExtensions.ToString (context.CurrentLine);
 			var last = path.LastIndexOfAny (FileDialog.Separators);
-			
-			if(string.IsNullOrWhiteSpace(path) || !Path.IsPathRooted(path)) {
+
+			if (string.IsNullOrWhiteSpace (path) || !Path.IsPathRooted (path)) {
 				return Enumerable.Empty<Suggestion> ();
 			}
 
 			var term = path.Substring (last + 1);
-			
+
 			// If path is /tmp/ then don't just list everything in it
-			if(string.IsNullOrWhiteSpace(term))
-			{
+			if (string.IsNullOrWhiteSpace (term)) {
 				return Enumerable.Empty<Suggestion> ();
 			}
 
@@ -52,7 +51,7 @@ namespace Terminal.Gui {
 
 			bool isWindows = RuntimeInformation.IsOSPlatform (OSPlatform.Windows);
 
-			var suggestions = state.Children.Where(d=> !d.IsParent).Select (
+			var suggestions = state.Children.Where (d => !d.IsParent).Select (
 				e => e.FileSystemInfo is IDirectoryInfo d
 					? d.Name + System.IO.Path.DirectorySeparatorChar
 					: e.FileSystemInfo.Name)
@@ -76,7 +75,7 @@ namespace Terminal.Gui {
 
 		public bool IsWordChar (Rune rune)
 		{
-			if (rune == '\n') {
+			if (rune.Value == '\n') {
 				return false;
 			}
 

+ 23 - 30
Terminal.Gui/Views/Button.cs

@@ -6,7 +6,7 @@
 //
 
 using System;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -58,7 +58,7 @@ namespace Terminal.Gui {
 		///   If <c>true</c>, a special decoration is used, and the user pressing the enter key 
 		///   in a <see cref="Dialog"/> will implicitly activate this button.
 		/// </param>
-		public Button (ustring text, bool is_default = false) : base (text)
+		public Button (string text, bool is_default = false) : base (text)
 		{
 			SetInitialProperties (text, is_default);
 		}
@@ -73,7 +73,7 @@ namespace Terminal.Gui {
 		/// <param name="x">X position where the button will be shown.</param>
 		/// <param name="y">Y position where the button will be shown.</param>
 		/// <param name="text">The button's text</param>
-		public Button (int x, int y, ustring text) : this (x, y, text, false) { }
+		public Button (int x, int y, string text) : this (x, y, text, false) { }
 
 		/// <summary>
 		///   Initializes a new instance of <see cref="Button"/> using <see cref="LayoutStyle.Absolute"/> layout, based on the given text.
@@ -89,8 +89,8 @@ namespace Terminal.Gui {
 		///   If <c>true</c>, a special decoration is used, and the user pressing the enter key 
 		///   in a <see cref="Dialog"/> will implicitly activate this button.
 		/// </param>
-		public Button (int x, int y, ustring text, bool is_default)
-		    : base (new Rect (x, y, text.RuneCount + 4 + (is_default ? 2 : 0), 1), text)
+		public Button (int x, int y, string text, bool is_default)
+		    : base (new Rect (x, y, text.GetRuneCount () + 4 + (is_default ? 2 : 0), 1), text)
 		{
 			SetInitialProperties (text, is_default);
 		}
@@ -100,17 +100,17 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="text"></param>
 		/// <param name="is_default"></param>
-		void SetInitialProperties (ustring text, bool is_default)
+		void SetInitialProperties (string text, bool is_default)
 		{
 			TextAlignment = TextAlignment.Centered;
 			VerticalTextAlignment = VerticalTextAlignment.Middle;
 
 			HotKeySpecifier = new Rune ('_');
 
-			_leftBracket = new Rune (Driver != null ? Driver.LeftBracket : '[');
-			_rightBracket = new Rune (Driver != null ? Driver.RightBracket : ']');
-			_leftDefault = new Rune (Driver != null ? Driver.LeftDefaultIndicator : '<');
-			_rightDefault = new Rune (Driver != null ? Driver.RightDefaultIndicator : '>');
+			_leftBracket = CM.Glyphs.LeftBracket;
+			_rightBracket = CM.Glyphs.RightBracket;
+			_leftDefault = CM.Glyphs.LeftDefaultIndicator;
+			_rightDefault = CM.Glyphs.RightDefaultIndicator;
 
 			CanFocus = true;
 			AutoSize = true;
@@ -151,7 +151,7 @@ namespace Terminal.Gui {
 					var v = value == Key.Unknown ? Key.Null : value;
 					if (base.HotKey != Key.Null && ContainsKeyBinding (Key.Space | base.HotKey)) {
 						if (v == Key.Null) {
-							ClearKeybinding (Key.Space | base.HotKey);
+							ClearKeyBinding (Key.Space | base.HotKey);
 						} else {
 							ReplaceKeyBinding (Key.Space | base.HotKey, Key.Space | v);
 						}
@@ -166,35 +166,28 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// 
 		/// </summary>
-		public bool NoDecorations {get;set;}
+		public bool NoDecorations { get; set; }
 
 		/// <summary>
 		/// 
 		/// </summary>
-		public bool NoPadding {get;set;}
+		public bool NoPadding { get; set; }
 
 		/// <inheritdoc/>
 		protected override void UpdateTextFormatterText ()
 		{
-			if(NoDecorations)
-			{
+			if (NoDecorations) {
 				TextFormatter.Text = Text;
-			}
-			else
+			} else
 			if (IsDefault)
-				TextFormatter.Text = ustring.Make (_leftBracket) + ustring.Make (_leftDefault) + " " + Text + " " + ustring.Make (_rightDefault) + ustring.Make (_rightBracket);
-			else
-			{
-				if(NoPadding)
-				{
-					TextFormatter.Text = ustring.Make (_leftBracket) + Text + ustring.Make (_rightBracket);
-				}
-				else
-				{
-					TextFormatter.Text = ustring.Make (_leftBracket) + " " + Text + " " + ustring.Make (_rightBracket);
+				TextFormatter.Text = $"{_leftBracket}{_leftDefault} {Text} {_rightDefault}{_rightBracket}";
+			else {
+				if (NoPadding) {
+					TextFormatter.Text = $"{_leftBracket}{Text}{_rightBracket}";
+				} else {
+					TextFormatter.Text = $"{_leftBracket} {Text} {_rightBracket}";
 				}
 			}
-				
 		}
 
 		///<inheritdoc/>
@@ -283,7 +276,7 @@ namespace Terminal.Gui {
 					if (!HasFocus) {
 						SetFocus ();
 						SetNeedsDisplay ();
-						Redraw (Bounds);
+						Draw ();
 					}
 					OnClicked ();
 				}
@@ -297,7 +290,7 @@ namespace Terminal.Gui {
 		public override void PositionCursor ()
 		{
 			if (HotKey == Key.Unknown && Text != "") {
-				for (int i = 0; i < TextFormatter.Text.RuneCount; i++) {
+				for (int i = 0; i < TextFormatter.Text.GetRuneCount (); i++) {
 					if (TextFormatter.Text [i] == Text [0]) {
 						Move (i, 0);
 						return;

+ 16 - 16
Terminal.Gui/Views/CheckBox.cs

@@ -5,7 +5,7 @@
 //   Miguel de Icaza ([email protected])
 //
 using System;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 
@@ -47,7 +47,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="s">S.</param>
 		/// <param name="is_checked">If set to <c>true</c> is checked.</param>
-		public CheckBox (ustring s, bool is_checked = false) : base ()
+		public CheckBox (string s, bool is_checked = false) : base ()
 		{
 			SetInitialProperties (s, is_checked);
 		}
@@ -59,7 +59,7 @@ namespace Terminal.Gui {
 		///   The size of <see cref="CheckBox"/> is computed based on the
 		///   text length. This <see cref="CheckBox"/> is not toggled.
 		/// </remarks>
-		public CheckBox (int x, int y, ustring s) : this (x, y, s, false)
+		public CheckBox (int x, int y, string s) : this (x, y, s, false)
 		{
 		}
 
@@ -70,7 +70,7 @@ namespace Terminal.Gui {
 		///   The size of <see cref="CheckBox"/> is computed based on the
 		///   text length. 
 		/// </remarks>
-		public CheckBox (int x, int y, ustring s, bool is_checked) : base (new Rect (x, y, s.Length, 1))
+		public CheckBox (int x, int y, string s, bool is_checked) : base (new Rect (x, y, s.Length, 1))
 		{
 			SetInitialProperties (s, is_checked);
 		}
@@ -81,17 +81,17 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <param name="s"></param>
 		/// <param name="is_checked"></param>
-		void SetInitialProperties (ustring s, bool is_checked)
+		void SetInitialProperties (string s, bool is_checked)
 		{
-			charNullChecked = new Rune (Driver != null ? Driver.NullChecked : '?');
-			charChecked = new Rune (Driver != null ? Driver.Checked : '√');
-			charUnChecked = new Rune (Driver != null ? Driver.UnChecked : '╴');
+			charNullChecked = CM.Glyphs.NullChecked;
+			charChecked = CM.Glyphs.Checked;
+			charUnChecked = CM.Glyphs.UnChecked;
 			Checked = is_checked;
-			HotKeySpecifier = new Rune ('_');
+			HotKeySpecifier = (Rune)'_';
 			CanFocus = true;
 			AutoSize = true;
 			Text = s;
-			
+
 			OnResizeNeeded ();
 
 			// Things this view knows how to do
@@ -109,10 +109,10 @@ namespace Terminal.Gui {
 			case TextAlignment.Left:
 			case TextAlignment.Centered:
 			case TextAlignment.Justified:
-				TextFormatter.Text = ustring.Make (GetCheckedState ()) + " " + GetFormatterText ();
+				TextFormatter.Text = $"{GetCheckedState ()} {GetFormatterText ()}";
 				break;
 			case TextAlignment.Right:
-				TextFormatter.Text = GetFormatterText () + " " + ustring.Make (GetCheckedState ());
+				TextFormatter.Text = $"{GetFormatterText ()} {GetCheckedState ()}";
 				break;
 			}
 		}
@@ -126,12 +126,12 @@ namespace Terminal.Gui {
 			}
 		}
 
-		ustring GetFormatterText ()
+		string GetFormatterText ()
 		{
-			if (AutoSize || ustring.IsNullOrEmpty (Text) || Frame.Width <= 2) {
+			if (AutoSize || string.IsNullOrEmpty (Text) || Frame.Width <= 2) {
 				return Text;
 			}
-			return Text.RuneSubstring (0, Math.Min (Frame.Width - 2, Text.RuneCount));
+			return Text [..Math.Min (Frame.Width - 2, Text.GetRuneCount ())];
 		}
 
 		/// <summary>
@@ -209,7 +209,7 @@ namespace Terminal.Gui {
 			} else {
 				Checked = !Checked;
 			}
-			
+
 			OnToggled (new ToggleEventArgs (previousChecked, Checked));
 			SetNeedsDisplay ();
 			return true;

+ 117 - 76
Terminal.Gui/Views/ColorPicker.cs

@@ -1,37 +1,80 @@
 using System;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 
+	/// <summary>
+	/// Event arguments for the <see cref="Color"/> events.
+	/// </summary>
+	public class ColorEventArgs : EventArgs {
+
+		/// <summary>
+		/// Initializes a new instance of <see cref="ColorEventArgs"/>
+		/// </summary>
+		public ColorEventArgs ()
+		{
+		}
+
+		/// <summary>
+		/// The new Thickness.
+		/// </summary>
+		public Color Color { get; set; }
+
+		/// <summary>
+		/// The previous Thickness.
+		/// </summary>
+		public Color PreviousColor { get; set; }
+	}
+
 	/// <summary>
 	/// The <see cref="ColorPicker"/> <see cref="View"/> Color picker.
 	/// </summary>
 	public class ColorPicker : View {
+		private int _selectColorIndex = (int)Color.Black;
+
 		/// <summary>
-		/// Number of colors on a line.
+		/// Columns of color boxes
 		/// </summary>
-		private static readonly int colorsPerLine = 8;
+		private int _cols = 8;
 
 		/// <summary>
-		/// Number of color lines.
+		/// Rows of color boxes
 		/// </summary>
-		private static readonly int lineCount = 2;
+		private int _rows = 2;
 
 		/// <summary>
-		/// Horizontal zoom.
+		/// Width of a color box
 		/// </summary>
-		private static readonly int horizontalZoom = 4;
+		public int BoxWidth {
+			get => _boxWidth;
+			set {
+				if (_boxWidth != value) {
+					_boxWidth = value;
+					SetNeedsLayout ();
+				}
+			}
+		}
+		private int _boxWidth = 4;
 
 		/// <summary>
-		/// Vertical zoom.
+		/// Height of a color box
 		/// </summary>
-		private static readonly int verticalZoom = 2;
+		public int BoxHeight {
+			get => _boxHeight;
+			set {
+				if (_boxHeight != value) {
+					_boxHeight = value;
+					SetNeedsLayout ();
+				}
+			}
+		}
+		private int _boxHeight = 2;
 
 		// Cursor runes.
-		private static readonly Rune [] cursorRunes = new Rune []
+		private static readonly Rune [] _cursorRunes = new Rune []
 		{
-			0x250C, 0x2500, 0x2500, 0x2510,
-			0x2514, 0x2500, 0x2500, 0x2518
+			(Rune)0x250C, (Rune) 0x2500, (Rune) 0x2500, (Rune) 0x2510,
+			(Rune) 0x2514, (Rune) 0x2500, (Rune) 0x2500, (Rune) 0x2518
 		};
 
 		/// <summary>
@@ -39,11 +82,11 @@ namespace Terminal.Gui {
 		/// </summary>
 		public Point Cursor {
 			get {
-				return new Point (selectColorIndex % colorsPerLine, selectColorIndex / colorsPerLine);
+				return new Point (_selectColorIndex % _cols, _selectColorIndex / _cols);
 			}
 
 			set {
-				var colorIndex = value.Y * colorsPerLine + value.X;
+				var colorIndex = value.Y * _cols + value.X;
 				SelectedColor = (Color)colorIndex;
 			}
 		}
@@ -51,21 +94,23 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Fired when a color is picked.
 		/// </summary>
-		public event EventHandler ColorChanged;
-
-		private int selectColorIndex = (int)Color.Black;
+		public event EventHandler<ColorEventArgs> ColorChanged;
 
 		/// <summary>
 		/// Selected color.
 		/// </summary>
 		public Color SelectedColor {
 			get {
-				return (Color)selectColorIndex;
+				return (Color)_selectColorIndex;
 			}
 
 			set {
-				selectColorIndex = (int)value;
-				ColorChanged?.Invoke (this, EventArgs.Empty);
+				Color prev = (Color)_selectColorIndex;
+				_selectColorIndex = (int)value;
+				ColorChanged?.Invoke (this, new ColorEventArgs () {
+					PreviousColor = prev,
+					Color = value,
+				});
 				SetNeedsDisplay ();
 			}
 		}
@@ -73,48 +118,19 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Initializes a new instance of <see cref="ColorPicker"/>.
 		/// </summary>
-		public ColorPicker () : base ("Color Picker")
-		{
-			Initialize ();
-		}
-
-		/// <summary>
-		/// Initializes a new instance of <see cref="ColorPicker"/>.
-		/// </summary>
-		/// <param name="title">Title.</param>
-		public ColorPicker (ustring title) : base (title)
+		public ColorPicker ()
 		{
-			Initialize ();
+			SetInitialProperties ();
 		}
 
-		/// <summary>
-		/// Initializes a new instance of <see cref="ColorPicker"/>.
-		/// </summary>
-		/// <param name="point">Location point.</param>
-		/// <param name="title">Title.</param>
-		public ColorPicker (Point point, ustring title) : this (point.X, point.Y, title)
-		{
-		}
-
-		/// <summary>
-		/// Initializes a new instance of <see cref="ColorPicker"/>.
-		/// </summary>
-		/// <param name="x">X location.</param>
-		/// <param name="y">Y location.</param>
-		/// <param name="title">Title</param>
-		public ColorPicker (int x, int y, ustring title) : base (x, y, title)
-		{
-			Initialize ();
-		}
-
-		private void Initialize()
+		private void SetInitialProperties ()
 		{
 			CanFocus = true;
-			Width = colorsPerLine * horizontalZoom;
-			Height = lineCount * verticalZoom + 1;
-
 			AddCommands ();
 			AddKeyBindings ();
+			LayoutStarted += (o, a) => {
+				Bounds = new Rect (Bounds.Location, new Size (_cols * BoxWidth, _rows * BoxHeight));
+			};
 		}
 
 		/// <summary>
@@ -140,16 +156,16 @@ namespace Terminal.Gui {
 		}
 
 		///<inheritdoc/>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
-			base.Redraw (bounds);
+			base.OnDrawContent (contentArea);
 
 			Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ());
 			var colorIndex = 0;
 
-			for (var y = 0; y < (Height.Anchor (0) - 1) / verticalZoom; y++) {
-				for (var x = 0; x < Width.Anchor (0) / horizontalZoom; x++) {
-					var foregroundColorIndex = y == 0 ? colorIndex + colorsPerLine : colorIndex - colorsPerLine;
+			for (var y = 0; y < (Bounds.Height / BoxHeight); y++) {
+				for (var x = 0; x < (Bounds.Width / BoxWidth); x++) {
+					var foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols;
 					Driver.SetAttribute (Driver.MakeAttribute ((Color)foregroundColorIndex, (Color)colorIndex));
 					var selected = x == Cursor.X && y == Cursor.Y;
 					DrawColorBox (x, y, selected);
@@ -168,20 +184,36 @@ namespace Terminal.Gui {
 		{
 			var index = 0;
 
-			for (var zommedY = 0; zommedY < verticalZoom; zommedY++) {
-				for (var zommedX = 0; zommedX < horizontalZoom; zommedX++) {
-					Move (x * horizontalZoom + zommedX, y * verticalZoom + zommedY + 1);
-
-					if (selected) {
-						var character = cursorRunes [index];
-						Driver.AddRune (character);
-					} else {
-						Driver.AddRune (' ');
-					}
-
+			for (var zoomedY = 0; zoomedY < BoxHeight; zoomedY++) {
+				for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++) {
+					Move (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY);
+					Driver.AddRune ((Rune)' ');
 					index++;
 				}
 			}
+
+			if (selected) {
+				DrawFocusRect (new Rect (x * BoxWidth, y * BoxHeight, BoxWidth, BoxHeight));
+			}
+		}
+
+		private void DrawFocusRect (Rect rect)
+		{
+			var lc = new LineCanvas ();
+			if (rect.Width == 1) {
+				lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted);
+			} else if (rect.Height == 1) {
+				lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted);
+			} else {
+				lc.AddLine (rect.Location, rect.Width, Orientation.Horizontal, LineStyle.Dotted);
+				lc.AddLine (new Point (rect.Location.X, rect.Location.Y + rect.Height - 1), rect.Width, Orientation.Horizontal, LineStyle.Dotted);
+
+				lc.AddLine (rect.Location, rect.Height, Orientation.Vertical, LineStyle.Dotted);
+				lc.AddLine (new Point (rect.Location.X + rect.Width - 1, rect.Location.Y), rect.Height, Orientation.Vertical, LineStyle.Dotted);
+			}
+			foreach (var p in lc.GetMap ()) {
+				AddRune (p.Key.X, p.Key.Y, p.Value);
+			}
 		}
 
 		/// <summary>
@@ -200,7 +232,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public virtual bool MoveRight ()
 		{
-			if (Cursor.X < colorsPerLine - 1) SelectedColor++;
+			if (Cursor.X < _cols - 1) SelectedColor++;
 			return true;
 		}
 
@@ -210,7 +242,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public virtual bool MoveUp ()
 		{
-			if (Cursor.Y > 0) SelectedColor -= colorsPerLine;
+			if (Cursor.Y > 0) SelectedColor -= _cols;
 			return true;
 		}
 
@@ -220,7 +252,7 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public virtual bool MoveDown ()
 		{
-			if (Cursor.Y < lineCount - 1) SelectedColor += colorsPerLine;
+			if (Cursor.Y < _rows - 1) SelectedColor += _cols;
 			return true;
 		}
 
@@ -237,13 +269,22 @@ namespace Terminal.Gui {
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent me)
 		{
-			if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) || !CanFocus)
+			if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) || !CanFocus) {
 				return false;
+			}
 
 			SetFocus ();
-			Cursor = new Point (me.X / horizontalZoom, (me.Y - 1) / verticalZoom);
+			Cursor = new Point ((me.X - GetFramesThickness ().Left) / _boxWidth, (me.Y - GetFramesThickness ().Top) / _boxHeight);
 
 			return true;
 		}
+
+		///<inheritdoc/>
+		public override bool OnEnter (View view)
+		{
+			Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
+
+			return base.OnEnter (view);
+		}
 	}
 }

+ 20 - 18
Terminal.Gui/Views/ComboBox.cs

@@ -8,7 +8,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -90,7 +90,7 @@ namespace Terminal.Gui {
 				return false;
 			}
 
-			public override void Redraw (Rect bounds)
+			public override void OnDrawContent (Rect contentArea)
 			{
 				var current = ColorScheme.Focus;
 				Driver.SetAttribute (current);
@@ -122,7 +122,7 @@ namespace Terminal.Gui {
 					Move (0, row);
 					if (Source == null || item >= Source.Count) {
 						for (int c = 0; c < f.Width; c++)
-							Driver.AddRune (' ');
+							Driver.AddRune ((Rune)' ');
 					} else {
 						var rowEventArgs = new ListViewRowEventArgs (item);
 						OnRowRender (rowEventArgs);
@@ -131,8 +131,8 @@ namespace Terminal.Gui {
 							Driver.SetAttribute (current);
 						}
 						if (AllowsMarking) {
-							Driver.AddRune (Source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
-							Driver.AddRune (' ');
+							Driver.AddRune (Source.IsMarked (item) ? (AllowsMultipleSelection ? CM.Glyphs.Checked : CM.Glyphs.Selected) : (AllowsMultipleSelection ? CM.Glyphs.UnChecked : CM.Glyphs.UnSelected));
+							Driver.AddRune ((Rune)' ');
 						}
 						Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
 					}
@@ -232,7 +232,7 @@ namespace Terminal.Gui {
 		public event EventHandler<ListViewItemEventArgs> OpenSelectedItem;
 
 		readonly IList searchset = new List<object> ();
-		ustring text = "";
+		string text = "";
 		readonly TextField search;
 		readonly ComboListView listview;
 		bool autoHide = true;
@@ -249,7 +249,7 @@ namespace Terminal.Gui {
 		/// Public constructor
 		/// </summary>
 		/// <param name="text"></param>
-		public ComboBox (ustring text) : base ()
+		public ComboBox (string text) : base ()
 		{
 			search = new TextField ("");
 			listview = new ComboListView (this, HideDropdownListOnClick) { LayoutStyle = LayoutStyle.Computed, CanFocus = true, TabStop = false };
@@ -484,7 +484,7 @@ namespace Terminal.Gui {
 				search.SetFocus ();
 			}
 
-			search.CursorPosition = search.Text.RuneCount;
+			search.CursorPosition = search.Text.GetRuneCount ();
 
 			return base.OnEnter (view);
 		}
@@ -534,9 +534,9 @@ namespace Terminal.Gui {
 		}
 
 		///<inheritdoc/>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
-			base.Redraw (bounds);
+			base.OnDrawContent (contentArea);
 
 			if (!autoHide) {
 				return;
@@ -544,7 +544,7 @@ namespace Terminal.Gui {
 
 			Driver.SetAttribute (ColorScheme.Focus);
 			Move (Bounds.Right - 1, 0);
-			Driver.AddRune (Driver.DownArrow);
+			Driver.AddRune (CM.Glyphs.DownArrow);
 		}
 
 		///<inheritdoc/>
@@ -627,7 +627,7 @@ namespace Terminal.Gui {
 
 			if (listview.HasFocus && listview.SelectedItem == 0 && searchset?.Count > 0) // jump back to search
 			{
-				search.CursorPosition = search.Text.RuneCount;
+				search.CursorPosition = search.Text.GetRuneCount ();
 				search.SetFocus ();
 				return true;
 			}
@@ -640,7 +640,9 @@ namespace Terminal.Gui {
 				if (searchset?.Count > 0) {
 					listview.TabStop = true;
 					listview.SetFocus ();
-					SetValue (searchset [listview.SelectedItem]);
+					if (listview.SelectedItem > -1) {
+						SetValue (searchset [listview.SelectedItem]);
+					}
 				} else {
 					listview.TabStop = false;
 					SuperView?.FocusNext ();
@@ -716,7 +718,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The currently selected list item
 		/// </summary>
-		public new ustring Text {
+		public new string Text {
 			get {
 				return text;
 			}
@@ -750,7 +752,7 @@ namespace Terminal.Gui {
 			}
 
 			SetValue (listview.SelectedItem > -1 ? searchset [listview.SelectedItem] : text);
-			search.CursorPosition = search.Text.ConsoleWidth;
+			search.CursorPosition = search.Text.GetColumns ();
 			Search_Changed (this, new TextChangedEventArgs (search.Text));
 			OnOpenSelectedItem ();
 			Reset (keepSearchText: true);
@@ -758,7 +760,7 @@ namespace Terminal.Gui {
 			isShow = false;
 		}
 
-		private int GetSelectedItemFromSource (ustring value)
+		private int GetSelectedItemFromSource (string value)
 		{
 			if (source == null) {
 				return -1;
@@ -814,14 +816,14 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			if (ustring.IsNullOrEmpty (search.Text) && ustring.IsNullOrEmpty (e.OldValue)) {
+			if (string.IsNullOrEmpty (search.Text) && string.IsNullOrEmpty (e.OldValue)) {
 				ResetSearchSet ();
 			} else if (search.Text != e.OldValue) {
 				isShow = true;
 				ResetSearchSet (noCopy: true);
 
 				foreach (var item in source.ToList ()) { // Iterate to preserver object type and force deep copy
-					if (item.ToString ().StartsWith (search.Text.ToString (), StringComparison.CurrentCultureIgnoreCase)) {
+					if (item.ToString ().StartsWith (search.Text, StringComparison.CurrentCultureIgnoreCase)) {
 						searchset.Add (item);
 					}
 				}

+ 5 - 14
Terminal.Gui/Views/ContextMenu.cs

@@ -33,7 +33,7 @@ namespace Terminal.Gui {
 		public ContextMenu () : this (0, 0, new MenuBarItem ()) { }
 
 		/// <summary>
-		/// Initializes a context menu, with a <see cref="View"/> specifiying the parent/hose of the menu.
+		/// Initializes a context menu, with a <see cref="View"/> specifying the parent/host of the menu.
 		/// </summary>
 		/// <param name="host">The host view.</param>
 		/// <param name="menuItems">The menu items for the context menu.</param>
@@ -79,7 +79,6 @@ namespace Terminal.Gui {
 			}
 			if (container != null) {
 				container.Closing -= Container_Closing;
-				container.TerminalResized -= Container_Resized;
 			}
 		}
 
@@ -91,13 +90,12 @@ namespace Terminal.Gui {
 			if (menuBar != null) {
 				Hide ();
 			}
-			container = Application.Top;
+			container = Application.Current;
 			container.Closing += Container_Closing;
-			container.TerminalResized += Container_Resized;
-			var frame = container.Frame;
+			var frame = new Rect (0, 0, View.Driver.Cols, View.Driver.Rows);
 			var position = Position;
 			if (Host != null) {
-				Host.ViewToScreen (container.Frame.X, container.Frame.Y, out int x, out int y);
+				Host.ViewToScreen (frame.X, frame.Y, out int x, out int y);
 				var pos = new Point (x, y);
 				pos.Y += Host.Frame.Height - 1;
 				if (position != pos) {
@@ -119,7 +117,7 @@ namespace Terminal.Gui {
 					if (Host == null) {
 						position.Y = frame.Bottom - rect.Height - 1;
 					} else {
-						Host.ViewToScreen (container.Frame.X, container.Frame.Y, out int x, out int y);
+						Host.ViewToScreen (frame.X, frame.Y, out int x, out int y);
 						var pos = new Point (x, y);
 						position.Y = pos.Y - rect.Height - 1;
 					}
@@ -145,13 +143,6 @@ namespace Terminal.Gui {
 			menuBar.OpenMenu ();
 		}
 
-		private void Container_Resized (object sender, SizeChangedEventArgs e)
-		{
-			if (IsShow) {
-				Show ();
-			}
-		}
-
 		private void Container_Closing (object sender, ToplevelClosingEventArgs obj)
 		{
 			Hide ();

+ 44 - 40
Terminal.Gui/Views/DateField.cs

@@ -8,7 +8,7 @@
 using System;
 using System.Globalization;
 using System.Linq;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -109,7 +109,7 @@ namespace Terminal.Gui {
 		void DateField_Changed (object sender, TextChangedEventArgs e)
 		{
 			try {
-				if (!DateTime.TryParseExact (GetDate (Text).ToString (), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result))
+				if (!DateTime.TryParseExact (GetDate (Text), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result))
 					Text = e.OldValue;
 			} catch (Exception) {
 				Text = e.OldValue;
@@ -123,13 +123,13 @@ namespace Terminal.Gui {
 
 		string GetLongFormat (string lf)
 		{
-			ustring [] frm = ustring.Make (lf).Split (ustring.Make (sepChar));
+			string [] frm = lf.Split (sepChar);
 			for (int i = 0; i < frm.Length; i++) {
-				if (frm [i].Contains ("M") && frm [i].RuneCount < 2)
+				if (frm [i].Contains ("M") && frm [i].GetRuneCount () < 2)
 					lf = lf.Replace ("M", "MM");
-				if (frm [i].Contains ("d") && frm [i].RuneCount < 2)
+				if (frm [i].Contains ("d") && frm [i].GetRuneCount () < 2)
 					lf = lf.Replace ("d", "dd");
-				if (frm [i].Contains ("y") && frm [i].RuneCount < 4)
+				if (frm [i].Contains ("y") && frm [i].GetRuneCount () < 4)
 					lf = lf.Replace ("yy", "yyyy");
 			}
 			return $" {lf}";
@@ -198,44 +198,44 @@ namespace Terminal.Gui {
 			newText.Add (key);
 			if (CursorPosition < fieldLen)
 				newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
-			return SetText (ustring.Make (newText));
+			return SetText (StringExtensions.ToString (newText));
 		}
 
-		bool SetText (ustring text)
+		bool SetText (string text)
 		{
-			if (text.IsEmpty) {
+			if (string.IsNullOrEmpty (text)) {
 				return false;
 			}
 
-			ustring [] vals = text.Split (ustring.Make (sepChar));
-			ustring [] frm = ustring.Make (format).Split (ustring.Make (sepChar));
+			string [] vals = text.Split (sepChar);
+			string [] frm = format.Split (sepChar);
 			bool isValidDate = true;
 			int idx = GetFormatIndex (frm, "y");
-			int year = Int32.Parse (vals [idx].ToString ());
+			int year = Int32.Parse (vals [idx]);
 			int month;
 			int day;
 			idx = GetFormatIndex (frm, "M");
-			if (Int32.Parse (vals [idx].ToString ()) < 1) {
+			if (Int32.Parse (vals [idx]) < 1) {
 				isValidDate = false;
 				month = 1;
 				vals [idx] = "1";
-			} else if (Int32.Parse (vals [idx].ToString ()) > 12) {
+			} else if (Int32.Parse (vals [idx]) > 12) {
 				isValidDate = false;
 				month = 12;
 				vals [idx] = "12";
 			} else
-				month = Int32.Parse (vals [idx].ToString ());
+				month = Int32.Parse (vals [idx]);
 			idx = GetFormatIndex (frm, "d");
-			if (Int32.Parse (vals [idx].ToString ()) < 1) {
+			if (Int32.Parse (vals [idx]) < 1) {
 				isValidDate = false;
 				day = 1;
 				vals [idx] = "1";
-			} else if (Int32.Parse (vals [idx].ToString ()) > 31) {
+			} else if (Int32.Parse (vals [idx]) > 31) {
 				isValidDate = false;
 				day = DateTime.DaysInMonth (year, month);
 				vals [idx] = day.ToString ();
 			} else
-				day = Int32.Parse (vals [idx].ToString ());
+				day = Int32.Parse (vals [idx]);
 			string d = GetDate (month, day, year, frm);
 
 			if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
@@ -245,7 +245,7 @@ namespace Terminal.Gui {
 			return true;
 		}
 
-		string GetDate (int month, int day, int year, ustring [] fm)
+		string GetDate (int month, int day, int year, string [] fm)
 		{
 			string date = " ";
 			for (int i = 0; i < fm.Length; i++) {
@@ -269,32 +269,31 @@ namespace Terminal.Gui {
 			return date;
 		}
 
-		ustring GetDate (ustring text)
+		string GetDate (string text)
 		{
-			ustring [] vals = text.Split (ustring.Make (sepChar));
-			ustring [] frm = ustring.Make (format).Split (ustring.Make (sepChar));
-			ustring [] date = { null, null, null };
+			string [] vals = text.Split (sepChar);
+			string [] frm = format.Split (sepChar);
+			string [] date = { null, null, null };
 
 			for (int i = 0; i < frm.Length; i++) {
 				if (frm [i].Contains ("M")) {
-					date [0] = vals [i].TrimSpace ();
+					date [0] = vals [i].Trim ();
 				} else if (frm [i].Contains ("d")) {
-					date [1] = vals [i].TrimSpace ();
+					date [1] = vals [i].Trim ();
 				} else {
-					var year = vals [i].TrimSpace ();
-					if (year.RuneCount == 2) {
+					var year = vals [i].Trim ();
+					if (year.GetRuneCount () == 2) {
 						var y = DateTime.Now.Year.ToString ();
 						date [2] = y.Substring (0, 2) + year.ToString ();
 					} else {
-						date [2] = vals [i].TrimSpace ();
+						date [2] = vals [i].Trim ();
 					}
 				}
 			}
-			return date [0] + ustring.Make (sepChar) + date [1] + ustring.Make (sepChar) + date [2];
-
+			return date [0] + sepChar + date [1] + sepChar + date [2];
 		}
 
-		int GetFormatIndex (ustring [] fm, string t)
+		int GetFormatIndex (string [] fm, string t)
 		{
 			int idx = -1;
 			for (int i = 0; i < fm.Length; i++) {
@@ -332,18 +331,22 @@ namespace Terminal.Gui {
 		public override bool ProcessKey (KeyEvent kb)
 		{
 			var result = InvokeKeybindings (kb);
-			if (result != null)
+			if (result != null) {
 				return (bool)result;
-
+			}
 			// Ignore non-numeric characters.
-			if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9'))
+			if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9')) {
 				return false;
+			}
 
-			if (ReadOnly)
+			if (ReadOnly) {
 				return true;
+			}
 
-			if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
+			// BUGBUG: This is a hack, we should be able to just use ((Rune)(uint)kb.Key) directly.
+			if (SetText (TextModel.ToRunes (((Rune)(uint)kb.Key).ToString ()).First ())) {
 				IncCursorPosition ();
+			}
 
 			return true;
 		}
@@ -376,10 +379,11 @@ namespace Terminal.Gui {
 		/// <inheritdoc/>
 		public override void DeleteCharLeft (bool useOldCursorPos = true)
 		{
-			if (ReadOnly)
+			if (ReadOnly) {
 				return;
+			}
 
-			SetText ('0');
+			SetText ((Rune)'0');
 			DecCursorPosition ();
 			return;
 		}
@@ -390,7 +394,7 @@ namespace Terminal.Gui {
 			if (ReadOnly)
 				return;
 
-			SetText ('0');
+			SetText ((Rune)'0');
 			return;
 		}
 
@@ -418,7 +422,7 @@ namespace Terminal.Gui {
 		/// <param name="args">Event arguments</param>
 		public virtual void OnDateChanged (DateTimeEventArgs<DateTime> args)
 		{
-			DateChanged?.Invoke (this,args);
+			DateChanged?.Invoke (this, args);
 		}
 	}
 }

+ 4 - 2
Terminal.Gui/Views/Dialog.cs

@@ -8,7 +8,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text.Json.Serialization;
-using NStack;
+using System.Text;
 using Terminal.Gui;
 using static Terminal.Gui.ConfigurationManager;
 
@@ -106,7 +106,9 @@ namespace Terminal.Gui {
 			buttons.Add (button);
 			Add (button);
 			SetNeedsDisplay ();
-			LayoutSubviews ();
+			if (IsInitialized) {
+				LayoutSubviews ();
+			}
 		}
 
 		// Get the width of all buttons, not including any Margin.

+ 45 - 10
Terminal.Gui/Views/FileDialog.cd

@@ -8,19 +8,19 @@
       <Compartment Name="Nested Types" Collapsed="false" />
     </Compartments>
     <NestedTypes>
-      <Class Name="Terminal.Gui.FileDialog.FileDialogSorter" Collapsed="true">
+      <Class Name="Terminal.Gui.FileDialog.SearchState" Collapsed="true">
         <TypeIdentifier>
           <NewMemberFileName>Windows\FileDialog.cs</NewMemberFileName>
         </TypeIdentifier>
       </Class>
-      <Class Name="Terminal.Gui.FileDialog.SearchState" Collapsed="true">
+      <Class Name="Terminal.Gui.FileDialog.FileDialogCollectionNavigator" Collapsed="true">
         <TypeIdentifier>
-          <NewMemberFileName>Windows\FileDialog.cs</NewMemberFileName>
+          <NewMemberFileName>Views\FileDialog.cs</NewMemberFileName>
         </TypeIdentifier>
       </Class>
     </NestedTypes>
     <TypeIdentifier>
-      <HashCode>goYYDAEnEDIZgHMByFAikQDFSIUQpUDABoZIFRSQwgQ=</HashCode>
+      <HashCode>g4YYDAEXEDKZgHMFyFAikQCFSKUQhRDABqJIlBSAwgw=</HashCode>
       <FileName>Views\FileDialog.cs</FileName>
     </TypeIdentifier>
     <ShowAsAssociation>
@@ -75,7 +75,7 @@
     </ShowAsCollectionAssociation>
   </Class>
   <Class Name="Terminal.Gui.FileDialogRootTreeNode" Collapsed="true">
-    <Position X="2.5" Y="8.25" Width="2" />
+    <Position X="2.5" Y="9" Width="2" />
     <TypeIdentifier>
       <HashCode>AAAAEAAAAAAAAAIEAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
       <FileName>FileServices\FileDialogRootTreeNode.cs</FileName>
@@ -84,7 +84,7 @@
   <Class Name="Terminal.Gui.FileDialogState">
     <Position X="11.5" Y="0.5" Width="1.5" />
     <TypeIdentifier>
-      <HashCode>AABAABAAAAAAAAAAAAAEQAAAAAAAQAAAAgAAAAAAAAI=</HashCode>
+      <HashCode>AABAABAAAAAAAAIAAAAEQAAAAAAAQAAAAgAAAAAAAAI=</HashCode>
       <FileName>FileServices\FileDialogState.cs</FileName>
     </TypeIdentifier>
     <ShowAsCollectionAssociation>
@@ -94,14 +94,14 @@
   <Class Name="Terminal.Gui.FileDialogStyle">
     <Position X="3.5" Y="0.5" Width="2.75" />
     <TypeIdentifier>
-      <HashCode>GgBIAAFEAAAAuAAAAAAEEACABAACKRkAAAEYACCAAAA=</HashCode>
+      <HashCode>GgBIAAFEAAAAuAAAAgAEEASABQACKRkAAAEYACCAAAA=</HashCode>
       <FileName>FileServices\FileDialogStyle.cs</FileName>
     </TypeIdentifier>
   </Class>
   <Class Name="Terminal.Gui.FileDialogTreeBuilder" BaseTypeListCollapsed="true">
     <Position X="0.5" Y="6.75" Width="1.75" />
     <TypeIdentifier>
-      <HashCode>EAAAAAAAAAAAAAAAQAAAAAAAAAAAQAAAAAAAAAQACAA=</HashCode>
+      <HashCode>EAAACAAAAAAAAAAAQAAAAAQAAAAAQAAAAAAAAAQACAA=</HashCode>
       <FileName>FileServices\FileDialogTreeBuilder.cs</FileName>
     </TypeIdentifier>
     <Lollipop Position="0.2" />
@@ -119,10 +119,38 @@
   <Class Name="Terminal.Gui.FileSystemInfoStats">
     <Position X="14" Y="0.5" Width="2.5" />
     <TypeIdentifier>
-      <HashCode>ABAIQAIIIAAAAAACQAAAAIQAAAQAAIAAAQABAAAYAAI=</HashCode>
+      <HashCode>ABAIQAIIIAAAAAACQAAAAIQAAAQAAIAAAQAAAAAIAAI=</HashCode>
       <FileName>FileServices\FileSystemInfoStats.cs</FileName>
     </TypeIdentifier>
   </Class>
+  <Class Name="Terminal.Gui.NerdFonts">
+    <Position X="9.25" Y="6.75" Width="2" />
+    <Compartments>
+      <Compartment Name="Fields" Collapsed="true" />
+    </Compartments>
+    <TypeIdentifier>
+      <HashCode>AIACAAABQAAAAAAAAAAAAAAAIACAAAAAAAIAAAQAAAA=</HashCode>
+      <FileName>Views\NerdFonts.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.FileDialogIconGetterArgs" Collapsed="true">
+    <Position X="6.75" Y="6.75" Width="2" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAgAAAAAAAAAQAAAAAAAEAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>FileServices\FileDialogIconGetterArgs.cs</FileName>
+    </TypeIdentifier>
+  </Class>
+  <Class Name="Terminal.Gui.FileDialogTableSource" Collapsed="true">
+    <Position X="4.75" Y="9" Width="2" />
+    <Compartments>
+      <Compartment Name="Fields" Collapsed="true" />
+    </Compartments>
+    <TypeIdentifier>
+      <HashCode>AQAAAAAAIAACAEAACAAAAAACAAAEAAAEAAAAgAgBBAA=</HashCode>
+      <FileName>Views\FileDialogTableSource.cs</FileName>
+    </TypeIdentifier>
+    <Lollipop Position="0.2" />
+  </Class>
   <Interface Name="Terminal.Gui.IAllowedType" Collapsed="true">
     <Position X="12" Y="4.25" Width="1.5" />
     <TypeIdentifier>
@@ -151,8 +179,15 @@
       <FileName>Views\OpenDialog.cs</FileName>
     </TypeIdentifier>
   </Enum>
+  <Enum Name="Terminal.Gui.FileDialogIconGetterContext">
+    <Position X="6.75" Y="7.25" Width="2.25" />
+    <TypeIdentifier>
+      <HashCode>AAAAAAAAAACAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAA=</HashCode>
+      <FileName>FileServices\FileDialogIconGetterContext.cs</FileName>
+    </TypeIdentifier>
+  </Enum>
   <Delegate Name="Terminal.Gui.FileDialogTreeRootGetter" Collapsed="true">
-    <Position X="2.5" Y="7.5" Width="2" />
+    <Position X="2.5" Y="8.25" Width="2" />
     <TypeIdentifier>
       <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAA=</HashCode>
       <FileName>FileServices\FileDialogRootTreeNode.cs</FileName>

+ 207 - 371
Terminal.Gui/Views/FileDialog.cs

@@ -7,12 +7,11 @@ using System.Linq;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
-using NStack;
+using System.Text;
 using Terminal.Gui.Resources;
 using static Terminal.Gui.ConfigurationManager;
 
 namespace Terminal.Gui {
-
 	/// <summary>
 	/// Modal dialog for selecting files/directories. Has auto-complete and expandable
 	/// navigation pane (Recent, Root drives etc).
@@ -24,7 +23,7 @@ namespace Terminal.Gui {
 		/// be made before the <see cref="Dialog"/> is loaded and shown to the user for the
 		/// first time.
 		/// </summary>
-		public FileDialogStyle Style { get; } = new FileDialogStyle ();
+		public FileDialogStyle Style { get; }
 
 		/// <summary>
 		/// The maximum number of results that will be collected
@@ -85,10 +84,8 @@ namespace Terminal.Gui {
 		private IFileSystem fileSystem;
 		private TextField tbPath;
 
-		private FileDialogSorter sorter;
 		private FileDialogHistory history;
 
-		private DataTable dtFiles;
 		private TableView tableView;
 		private TreeView<object> treeView;
 		private TileView splitContainer;
@@ -99,15 +96,15 @@ namespace Terminal.Gui {
 		private Button btnBack;
 		private Button btnUp;
 		private string feedback;
-
-		private CollectionNavigator collectionNavigator = new CollectionNavigator ();
-
 		private TextField tbFind;
 		private SpinnerView spinnerView;
 		private MenuBar allowedTypeMenuBar;
 		private MenuBarItem allowedTypeMenu;
 		private MenuItem [] allowedTypeMenuItems;
-		private DataColumn filenameColumn;
+
+		private int currentSortColumn;
+
+		private bool currentSortIsAsc = true;
 
 		/// <summary>
 		/// Event fired when user attempts to confirm a selection (or multi selection).
@@ -127,7 +124,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Initializes a new instance of the <see cref="FileDialog"/> class.
 		/// </summary>
-		public FileDialog () : this(new FileSystem())
+		public FileDialog () : this (new FileSystem ())
 		{
 
 		}
@@ -140,6 +137,8 @@ namespace Terminal.Gui {
 		public FileDialog (IFileSystem fileSystem)
 		{
 			this.fileSystem = fileSystem;
+			Style = new FileDialogStyle (fileSystem);
+
 			this.btnOk = new Button (Style.OkButtonText) {
 				Y = Pos.AnchorEnd (1),
 				X = Pos.Function (() =>
@@ -183,7 +182,7 @@ namespace Terminal.Gui {
 			this.btnBack.Clicked += (s, e) => this.history.Back ();
 
 			this.btnForward = new Button () { X = Pos.Right (btnBack) + 1, Y = 1, NoPadding = true };
-			btnForward.Text = GetForwardButtonText();
+			btnForward.Text = GetForwardButtonText ();
 			this.btnForward.Clicked += (s, e) => this.history.Forward ();
 
 			this.tbPath = new TextField {
@@ -210,23 +209,40 @@ namespace Terminal.Gui {
 				Height = Dim.Fill (1),
 			};
 			this.splitContainer.SetSplitterPos (0, 30);
-//			this.splitContainer.Border.BorderStyle = BorderStyle.None;
+			//			this.splitContainer.Border.BorderStyle = BorderStyle.None;
 			this.splitContainer.Tiles.ElementAt (0).ContentView.Visible = false;
 
-			this.tableView = new TableView () {
+			this.tableView = new TableView {
 				Width = Dim.Fill (),
 				Height = Dim.Fill (),
 				FullRowSelect = true,
+				CollectionNavigator = new FileDialogCollectionNavigator (this)
 			};
-
 			this.tableView.AddKeyBinding (Key.Space, Command.ToggleChecked);
+			this.tableView.MouseClick += OnTableViewMouseClick;
 			Style.TableStyle = tableView.Style;
 
+			var nameStyle = Style.TableStyle.GetOrCreateColumnStyle (0);
+			nameStyle.MinWidth = 10;
+			nameStyle.ColorGetter = this.ColorGetter;
+
+			var sizeStyle = Style.TableStyle.GetOrCreateColumnStyle (1);
+			sizeStyle.MinWidth = 10;
+			sizeStyle.ColorGetter = this.ColorGetter;
+
+			var dateModifiedStyle = Style.TableStyle.GetOrCreateColumnStyle (2);
+			dateModifiedStyle.MinWidth = 30;
+			dateModifiedStyle.ColorGetter = this.ColorGetter;
+
+			var typeStyle = Style.TableStyle.GetOrCreateColumnStyle (3);
+			typeStyle.MinWidth = 6;
+			typeStyle.ColorGetter = this.ColorGetter;
+
 			this.tableView.KeyPress += (s, k) => {
 				if (this.tableView.SelectedRow <= 0) {
 					this.NavigateIf (k, Key.CursorUp, this.tbPath);
 				}
-				if (this.tableView.SelectedRow == this.tableView.Table.Rows.Count-1) {
+				if (this.tableView.SelectedRow == this.tableView.Table.Rows - 1) {
 					this.NavigateIf (k, Key.CursorDown, this.btnToggleSplitterCollapse);
 				}
 
@@ -237,14 +253,6 @@ namespace Terminal.Gui {
 				if (k.Handled) {
 					return;
 				}
-
-				if (this.tableView.HasFocus &&
-				!k.KeyEvent.Key.HasFlag (Key.CtrlMask) &&
-				!k.KeyEvent.Key.HasFlag (Key.AltMask) &&
-					char.IsLetterOrDigit ((char)k.KeyEvent.KeyValue)) {
-					CycleToNextTableEntryBeginningWith (k);
-				}
-
 			};
 
 			this.treeView = new TreeView<object> () {
@@ -252,8 +260,9 @@ namespace Terminal.Gui {
 				Height = Dim.Fill (),
 			};
 
-			this.treeView.TreeBuilder = new FileDialogTreeBuilder ();
-			this.treeView.AspectGetter = (m) => m is IDirectoryInfo d ? d.Name : m.ToString ();
+			var fileDialogTreeBuilder = new FileDialogTreeBuilder (this);
+			this.treeView.TreeBuilder = fileDialogTreeBuilder;
+			this.treeView.AspectGetter = fileDialogTreeBuilder.AspectGetter;
 			this.Style.TreeStyle = treeView.Style;
 
 			this.treeView.SelectionChanged += this.TreeView_SelectionChanged;
@@ -270,7 +279,7 @@ namespace Terminal.Gui {
 				var newState = !tile.ContentView.Visible;
 				tile.ContentView.Visible = newState;
 				this.btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState);
-				this.LayoutSubviews();
+				this.LayoutSubviews ();
 			};
 
 			tbFind = new TextField {
@@ -293,12 +302,12 @@ namespace Terminal.Gui {
 					o.Handled = true;
 				}
 
-				if(o.KeyEvent.Key == Key.Esc) {
-					if(CancelSearch()) {
+				if (o.KeyEvent.Key == Key.Esc) {
+					if (CancelSearch ()) {
 						o.Handled = true;
 					}
 				}
-				if(tbFind.CursorIsAtEnd()) {
+				if (tbFind.CursorIsAtEnd ()) {
 					NavigateIf (o, Key.CursorRight, btnCancel);
 				}
 				if (tbFind.CursorIsAtStart ()) {
@@ -313,13 +322,8 @@ namespace Terminal.Gui {
 			this.tableView.Style.ShowHorizontalHeaderUnderline = true;
 			this.tableView.Style.ShowHorizontalScrollIndicators = true;
 
-			this.SetupTableColumns ();
-
-			this.sorter = new FileDialogSorter (this, this.tableView);
 			this.history = new FileDialogHistory (this);
 
-			this.tableView.Table = this.dtFiles;
-
 			this.tbPath.TextChanged += (s, e) => this.PathChanged ();
 
 			this.tableView.CellActivated += this.CellActivate;
@@ -366,19 +370,39 @@ namespace Terminal.Gui {
 			this.Add (this.btnForward);
 			this.Add (this.tbPath);
 			this.Add (this.splitContainer);
+		}
+
+		private void OnTableViewMouseClick (object sender, MouseEventEventArgs e)
+		{
+			var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
+
+			if (clickedCol != null) {
+				if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
+
+					// left click in a header
+					this.SortColumn (clickedCol.Value);
+				} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
 
-			// Default sort order is by name
-			sorter.SortColumn(this.filenameColumn,true);
+					// right click in a header
+					this.ShowHeaderContextMenu (clickedCol.Value, e);
+				}
+			} else {
+				if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
+
+					// right click in rest of table
+					this.ShowCellContextMenu (clickedCell, e);
+				}
+			}
 		}
 
 		private string GetForwardButtonText ()
 		{
-			return "-" + Driver.RightArrow;
+			return "-" + CM.Glyphs.RightArrow;
 		}
 
 		private string GetBackButtonText ()
 		{
-			return Driver.LeftArrow + "-";
+			return CM.Glyphs.LeftArrow + "-";
 		}
 
 		private string GetUpButtonText ()
@@ -388,9 +412,9 @@ namespace Terminal.Gui {
 
 		private string GetToggleSplitterText (bool isExpanded)
 		{
-			return isExpanded ? 
-				new string ((char)Driver.LeftArrow, 2) :
-				new string ((char)Driver.RightArrow, 2);
+			return isExpanded ?
+				new string ((char)CM.Glyphs.LeftArrow.Value, 2) :
+				new string ((char)CM.Glyphs.RightArrow.Value, 2);
 		}
 
 		private void Delete ()
@@ -486,7 +510,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			PushState (new SearchState (State?.Directory, this, tbFind.Text.ToString ()), true);
+			PushState (new SearchState (State?.Directory, this, tbFind.Text), true);
 		}
 
 		/// <inheritdoc/>
@@ -512,48 +536,6 @@ namespace Terminal.Gui {
 			feedback = null;
 		}
 
-		private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent)
-		{
-			if (tableView.Table.Rows.Count == 0) {
-				return;
-			}
-
-			var row = tableView.SelectedRow;
-
-			// There is a multi select going on and not just for the current row
-			if (tableView.GetAllSelectedCells ().Any (c => c.Y != row)) {
-				return;
-			}
-
-			int match = collectionNavigator.GetNextMatchingItem (row, (char)keyEvent.KeyEvent.KeyValue);
-
-			if (match != -1) {
-				tableView.SelectedRow = match;
-				tableView.EnsureValidSelection ();
-				tableView.EnsureSelectedCellIsVisible ();
-				keyEvent.Handled = true;
-			}
-		}
-
-		private void UpdateCollectionNavigator ()
-		{
-			tableView.EnsureValidSelection ();
-			var col = tableView.SelectedColumn;
-			var style = tableView.Style.GetColumnStyleIfAny (tableView.Table.Columns [col]);
-
-
-			var collection = tableView
-				.Table
-				.Rows
-				.Cast<DataRow> ()
-				.Select ((o, idx) => col == 0 ? 
-					RowToStats(idx).FileSystemInfo.Name :
-					style.GetRepresentation (o [0])?.TrimStart('.'))
-				.ToArray ();
-
-			collectionNavigator = new CollectionNavigator (collection);
-		}
-
 		/// <summary>
 		/// Gets or Sets which <see cref="System.IO.FileSystemInfo"/> type can be selected.
 		/// Defaults to <see cref="OpenMode.Mixed"/> (i.e. <see cref="DirectoryInfo"/> or
@@ -567,7 +549,7 @@ namespace Terminal.Gui {
 		/// is true.
 		/// </summary>
 		public string Path {
-			get => this.tbPath.Text.ToString ();
+			get => this.tbPath.Text;
 			set {
 				this.tbPath.Text = value;
 				this.tbPath.MoveEnd ();
@@ -613,19 +595,19 @@ namespace Terminal.Gui {
 			= Enumerable.Empty<string> ().ToList ().AsReadOnly ();
 
 		/// <inheritdoc/>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
-			base.Redraw (bounds);
+			base.OnDrawContent (contentArea);
 
 			if (!string.IsNullOrWhiteSpace (feedback)) {
-				var feedbackWidth = feedback.Sum (c => Rune.ColumnWidth (c));
-				var feedbackPadLeft = ((bounds.Width - feedbackWidth) / 2) - 1;
+				var feedbackWidth = feedback.EnumerateRunes ().Sum (c => c.GetColumns ());
+				var feedbackPadLeft = ((Bounds.Width - feedbackWidth) / 2) - 1;
 
-				feedbackPadLeft = Math.Min (bounds.Width, feedbackPadLeft);
+				feedbackPadLeft = Math.Min (Bounds.Width, feedbackPadLeft);
 				feedbackPadLeft = Math.Max (0, feedbackPadLeft);
 
-				var feedbackPadRight = bounds.Width - (feedbackPadLeft + feedbackWidth + 2);
-				feedbackPadRight = Math.Min (bounds.Width, feedbackPadRight);
+				var feedbackPadRight = Bounds.Width - (feedbackPadLeft + feedbackWidth + 2);
+				feedbackPadRight = Math.Min (Bounds.Width, feedbackPadRight);
 				feedbackPadRight = Math.Max (0, feedbackPadRight);
 
 				Move (0, Bounds.Height / 2);
@@ -648,11 +630,11 @@ namespace Terminal.Gui {
 
 			// May have been updated after instance was constructed
 			this.btnOk.Text = Style.OkButtonText;
-			this.btnUp.Text = this.GetUpButtonText();
-			this.btnBack.Text = this.GetBackButtonText();
-			this.btnForward.Text = this.GetForwardButtonText();
-			this.btnToggleSplitterCollapse.Text = this.GetToggleSplitterText(false);
-			
+			this.btnUp.Text = this.GetUpButtonText ();
+			this.btnBack.Text = this.GetBackButtonText ();
+			this.btnForward.Text = this.GetForwardButtonText ();
+			this.btnToggleSplitterCollapse.Text = this.GetToggleSplitterText (false);
+
 			tbPath.Autocomplete.ColorScheme.Normal = Attribute.Make (Color.Black, tbPath.ColorScheme.Normal.Background);
 
 			treeView.AddObjects (Style.TreeRootGetter ());
@@ -690,9 +672,9 @@ namespace Terminal.Gui {
 				};
 
 				allowedTypeMenuBar.DrawContentComplete += (s, e) => {
-					
+
 					allowedTypeMenuBar.Move (e.Rect.Width - 1, 0);
-					Driver.AddRune (Driver.DownArrow);
+					Driver.AddRune (CM.Glyphs.DownArrow);
 
 				};
 
@@ -710,7 +692,7 @@ namespace Terminal.Gui {
 			this.tbPath.FocusFirst ();
 			this.tbPath.SelectAll ();
 
-			if (ustring.IsNullOrEmpty (Title)) {
+			if (string.IsNullOrEmpty (Title)) {
 				switch (OpenMode) {
 				case OpenMode.File:
 					this.Title = $"{Strings.fdOpen} {(MustExist ? Strings.fdExisting + " " : "")}{Strings.fdFile}";
@@ -788,7 +770,7 @@ namespace Terminal.Gui {
 
 			// Don't include ".." (IsParent) in multiselections
 			this.MultiSelected = toMultiAccept
-				.Where(s=>!s.IsParent)
+				.Where (s => !s.IsParent)
 				.Select (s => s.FileSystemInfo.FullName)
 				.ToList ().AsReadOnly ();
 
@@ -816,12 +798,11 @@ namespace Terminal.Gui {
 
 		private void Accept (bool allowMulti)
 		{
-			if(allowMulti && TryAcceptMulti())
-			{
+			if (allowMulti && TryAcceptMulti ()) {
 				return;
 			}
 
-			if (!this.IsCompatibleWithOpenMode (this.tbPath.Text.ToString (), out string reason)) {
+			if (!this.IsCompatibleWithOpenMode (this.tbPath.Text, out string reason)) {
 				if (reason != null) {
 					feedback = reason;
 					SetNeedsDisplay ();
@@ -897,7 +878,7 @@ namespace Terminal.Gui {
 
 		private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEventArgs obj)
 		{
-			if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows.Count == 0) {
+			if (!this.tableView.HasFocus || obj.NewRow == -1 || obj.Table.Rows == 0) {
 				return;
 			}
 
@@ -929,10 +910,6 @@ namespace Terminal.Gui {
 
 				this.pushingState = false;
 			}
-
-			if (obj.NewCol != obj.OldCol) {
-				UpdateCollectionNavigator ();
-			}
 		}
 
 		private bool TableView_KeyUp (KeyEvent keyEvent)
@@ -964,62 +941,9 @@ namespace Terminal.Gui {
 			return false;
 		}
 
-		private void SetupTableColumns ()
-		{
-			this.dtFiles = new DataTable ();
-
-			var nameStyle = this.tableView.Style.GetOrCreateColumnStyle (
-				filenameColumn = this.dtFiles.Columns.Add (Style.FilenameColumnName, typeof (int))
-				);
-			nameStyle.RepresentationGetter = (i) => {
-
-				var stats = this.State?.Children [(int)i];
-
-				if (stats == null) {
-					return string.Empty;
-				}
-
-				var icon = stats.IsParent ? null : Style.IconGetter?.Invoke (stats.FileSystemInfo);
-
-				if (icon != null) {
-					return icon + stats.Name;
-				}
-				return stats.Name;
-			};
-
-			nameStyle.MinWidth = 50;
-
-			var sizeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.SizeColumnName, typeof (int)));
-			sizeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].HumanReadableLength ?? string.Empty;
-			nameStyle.MinWidth = 10;
-
-			var dateModifiedStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.ModifiedColumnName, typeof (int)));
-			dateModifiedStyle.RepresentationGetter = (i) => 
-			{
-				var s = this.State?.Children [(int)i];
-				if(s == null || s.IsParent || s.LastWriteTime == null)
-				{
-					return string.Empty;
-				}
-				return s.LastWriteTime.Value.ToString (Style.DateFormat);
-			};
-
-			dateModifiedStyle.MinWidth = 30;
-
-			var typeStyle = this.tableView.Style.GetOrCreateColumnStyle (this.dtFiles.Columns.Add (Style.TypeColumnName, typeof (int)));
-			typeStyle.RepresentationGetter = (i) => this.State?.Children [(int)i].Type ?? string.Empty;
-			typeStyle.MinWidth = 6;
-
-			foreach(var colStyle in Style.TableStyle.ColumnStyles) {
-				colStyle.Value.ColorGetter = this.ColorGetter;
-			}
-			
-		}
-
 		private void CellActivate (object sender, CellActivatedEventArgs obj)
 		{
-			if(TryAcceptMulti())
-			{
+			if (TryAcceptMulti ()) {
 				return;
 			}
 
@@ -1039,20 +963,16 @@ namespace Terminal.Gui {
 		{
 			var multi = this.MultiRowToStats ();
 			string reason = null;
-			
-			if (!multi.Any ())
-			{
+
+			if (!multi.Any ()) {
 				return false;
 			}
-			
+
 			if (multi.All (m => this.IsCompatibleWithOpenMode (
-				m.FileSystemInfo.FullName, out reason)))
-			{
+				m.FileSystemInfo.FullName, out reason))) {
 				this.Accept (multi);
 				return true;
-			} 
-			else 
-			{
+			} else {
 				if (reason != null) {
 					feedback = reason;
 					SetNeedsDisplay ();
@@ -1184,12 +1104,10 @@ namespace Terminal.Gui {
 
 				this.tbPath.Autocomplete.ClearSuggestions ();
 
-				if(pathText != null)
-				{
+				if (pathText != null) {
 					this.tbPath.Text = pathText;
 					this.tbPath.MoveEnd ();
-				}
-				else
+				} else
 				if (setPathText) {
 					this.tbPath.Text = newState.Directory.FullName;
 					this.tbPath.MoveEnd ();
@@ -1223,24 +1141,13 @@ namespace Terminal.Gui {
 			if (this.State == null) {
 				return;
 			}
+			this.tableView.Table = new FileDialogTableSource (this, this.State, this.Style, currentSortColumn, currentSortIsAsc);
 
-			this.dtFiles.Rows.Clear ();
-
-			for (int i = 0; i < this.State.Children.Length; i++) {
-				this.BuildRow (i);
-			}
-
-			this.sorter.ApplySort ();
+			this.ApplySort ();
 			this.tableView.Update ();
-			UpdateCollectionNavigator ();
-		}
-
-		private void BuildRow (int idx)
-		{
-			this.tableView.Table.Rows.Add (idx, idx, idx, idx);
 		}
 
-		private ColorScheme ColorGetter (TableView.CellColorGetterArgs args)
+		private ColorScheme ColorGetter (CellColorGetterArgs args)
 		{
 			var stats = this.RowToStats (args.RowIndex);
 
@@ -1277,7 +1184,7 @@ namespace Terminal.Gui {
 
 				foreach (var p in this.tableView.GetAllSelectedCells ()) {
 
-					var add = this.State?.Children [(int)this.tableView.Table.Rows [p.Y] [0]];
+					var add = this.State?.Children [p.Y];
 					if (add != null) {
 						toReturn.Add (add);
 					}
@@ -1288,29 +1195,7 @@ namespace Terminal.Gui {
 		}
 		private FileSystemInfoStats RowToStats (int rowIndex)
 		{
-			return this.State?.Children [(int)this.tableView.Table.Rows [rowIndex] [0]];
-		}
-		private int? StatsToRow (IFileSystemInfo fileSystemInfo)
-		{
-			// find array index of the current state for the stats
-			var idx = State?.Children.IndexOf ((f) => f.FileSystemInfo.FullName == fileSystemInfo.FullName);
-
-			if (idx != -1 && idx != null) {
-
-				// find the row number in our DataTable where the cell
-				// contains idx
-				var match = tableView.Table.Rows
-					.Cast<DataRow> ()
-					.Select ((r, rIdx) => new { row = r, rowIdx = rIdx })
-					.Where (t => (int)t.row [0] == idx)
-					.ToArray ();
-
-				if (match.Length == 1) {
-					return match [0].rowIdx;
-				}
-			}
-
-			return null;
+			return this.State?.Children [rowIndex];
 		}
 
 		private void PathChanged ()
@@ -1320,7 +1205,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			var path = this.tbPath.Text?.ToString ();
+			var path = this.tbPath.Text;
 
 			if (string.IsNullOrWhiteSpace (path)) {
 				return;
@@ -1344,10 +1229,10 @@ namespace Terminal.Gui {
 			// where the FullName is in fact the current working directory.
 			// really not what most users would expect
 			if (Regex.IsMatch (path, "^\\w:$")) {
-				return fileSystem.DirectoryInfo.New(path + System.IO.Path.DirectorySeparatorChar);
+				return fileSystem.DirectoryInfo.New (path + System.IO.Path.DirectorySeparatorChar);
 			}
 
-			return fileSystem.DirectoryInfo.New(path);
+			return fileSystem.DirectoryInfo.New (path);
 		}
 
 		/// <summary>
@@ -1356,181 +1241,109 @@ namespace Terminal.Gui {
 		/// <param name="toRestore"></param>
 		internal void RestoreSelection (IFileSystemInfo toRestore)
 		{
-			var toReselect = StatsToRow (toRestore);
-
-			if (toReselect.HasValue) {
-				tableView.SelectedRow = toReselect.Value;
-				tableView.EnsureSelectedCellIsVisible ();
-			}
+			tableView.SelectedRow = State.Children.IndexOf (r => r.FileSystemInfo == toRestore);
+			tableView.EnsureSelectedCellIsVisible ();
 		}
-		private class FileDialogSorter {
-			private readonly FileDialog dlg;
-			private TableView tableView;
-
-			private DataColumn currentSort = null;
-			private bool currentSortIsAsc = true;
-
-			public FileDialogSorter (FileDialog dlg, TableView tableView)
-			{
-				this.dlg = dlg;
-				this.tableView = tableView;
-
-				// if user clicks the mouse in TableView
-				this.tableView.MouseClick += (s, e) => {
 
-					var clickedCell = this.tableView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out DataColumn clickedCol);
-
-					if (clickedCol != null) {
-						if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
-
-							// left click in a header
-							this.SortColumn (clickedCol);
-						} else if (e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
-
-							// right click in a header
-							this.ShowHeaderContextMenu (clickedCol, e);
-						}
-					} else {
-						if (clickedCell != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) {
-
-							// right click in rest of table
-							this.ShowCellContextMenu (clickedCell, e);
-						}
-					}
-
-				};
-
-			}
-
-			internal void ApplySort ()
-			{
-				var col = this.currentSort;
-
-				// TODO: Consider preserving selection
-				this.tableView.Table.Rows.Clear ();
-
-				var colName = col == null ? null : StripArrows (col.ColumnName);
-
-				var stats = this.dlg.State?.Children ?? new FileSystemInfoStats [0];
-
-				// Do we sort on a column or just use the default sort order?
-				Func<FileSystemInfoStats, object> sortAlgorithm;
-
-				if (colName == null) {
-					sortAlgorithm = (v) => v.GetOrderByDefault ();
-					this.currentSortIsAsc = true;
-				} else {
-					sortAlgorithm = (v) => v.GetOrderByValue (dlg, colName);
-				}
-
-				// This portion is never reordered (aways .. at top then folders)
-				var forcedOrder = stats.Select ((v, i) => new { v, i })
-						.OrderByDescending (f => f.v.IsParent)
-						.ThenBy (f => f.v.IsDir() ? -1:100);
+		internal void ApplySort ()
+		{
+			var stats = State?.Children ?? new FileSystemInfoStats [0];
 
-				// This portion is flexible based on the column clicked (e.g. alphabetical)
-				var ordered = 
-					this.currentSortIsAsc ?
-					    forcedOrder.ThenBy (f => sortAlgorithm (f.v)):
-						forcedOrder.ThenByDescending (f => sortAlgorithm (f.v));
+			// This portion is never reordered (aways .. at top then folders)
+			var forcedOrder = stats
+			.OrderByDescending (f => f.IsParent)
+					.ThenBy (f => f.IsDir () ? -1 : 100);
 
-				foreach (var o in ordered) {
-					this.dlg.BuildRow (o.i);
-				}
+			// This portion is flexible based on the column clicked (e.g. alphabetical)
+			var ordered =
+				this.currentSortIsAsc ?
+					forcedOrder.ThenBy (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f)) :
+					forcedOrder.ThenByDescending (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f));
 
-				foreach (DataColumn c in this.tableView.Table.Columns) {
+			State.Children = ordered.ToArray ();
 
-					// remove any lingering sort indicator
-					c.ColumnName = StripArrows (c.ColumnName);
+			this.tableView.Update ();
+		}
 
-					// add a new one if this the one that is being sorted
-					if (c == col) {
-						c.ColumnName += this.currentSortIsAsc ? " (▲)" : " (▼)";
-					}
-				}
+		private void SortColumn (int clickedCol)
+		{
+			this.GetProposedNewSortOrder (clickedCol, out var isAsc);
+			this.SortColumn (clickedCol, isAsc);
+			this.tableView.Table = new FileDialogTableSource (this, State, Style, currentSortColumn, currentSortIsAsc);
+		}
 
-				this.tableView.Update ();
-				dlg.UpdateCollectionNavigator ();
-			}
+		internal void SortColumn (int col, bool isAsc)
+		{
+			// set a sort order
+			this.currentSortColumn = col;
+			this.currentSortIsAsc = isAsc;
 
-			private static string StripArrows (string columnName)
-			{
-				return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
-			}
+			this.ApplySort ();
+		}
 
-			private void SortColumn (DataColumn clickedCol)
-			{
-				this.GetProposedNewSortOrder (clickedCol, out var isAsc);
-				this.SortColumn (clickedCol, isAsc);
+		private string GetProposedNewSortOrder (int clickedCol, out bool isAsc)
+		{
+			// work out new sort order
+			if (this.currentSortColumn == clickedCol && this.currentSortIsAsc) {
+				isAsc = false;
+				return $"{tableView.Table.ColumnNames [clickedCol]} DESC";
+			} else {
+				isAsc = true;
+				return $"{tableView.Table.ColumnNames [clickedCol]} ASC";
 			}
+		}
 
-			internal void SortColumn (DataColumn col, bool isAsc)
-			{
-				// set a sort order
-				this.currentSort = col;
-				this.currentSortIsAsc = isAsc;
-
-				this.ApplySort ();
-			}
+		private void ShowHeaderContextMenu (int clickedCol, MouseEventEventArgs e)
+		{
+			var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc);
 
-			private string GetProposedNewSortOrder (DataColumn clickedCol, out bool isAsc)
-			{
-				// work out new sort order
-				if (this.currentSort == clickedCol && this.currentSortIsAsc) {
-					isAsc = false;
-					return $"{clickedCol.ColumnName} DESC";
-				} else {
-					isAsc = true;
-					return $"{clickedCol.ColumnName} ASC";
-				}
-			}
+			var contextMenu = new ContextMenu (
+				e.MouseEvent.X + 1,
+				e.MouseEvent.Y + 1,
+				new MenuBarItem (new MenuItem []
+				{
+					new MenuItem($"Hide {StripArrows(tableView.Table.ColumnNames[clickedCol])}", string.Empty, () => this.HideColumn(clickedCol)),
+					new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)),
+				})
+			);
 
-			private void ShowHeaderContextMenu (DataColumn clickedCol, MouseEventEventArgs e)
-			{
-				var sort = this.GetProposedNewSortOrder (clickedCol, out var isAsc);
+			contextMenu.Show ();
+		}
 
-				var contextMenu = new ContextMenu (
-					e.MouseEvent.X + 1,
-					e.MouseEvent.Y + 1,
-					new MenuBarItem (new MenuItem []
-					{
-						new MenuItem($"Hide {StripArrows(clickedCol.ColumnName)}", string.Empty, () => this.HideColumn(clickedCol)),
-						new MenuItem($"Sort {StripArrows(sort)}",string.Empty, ()=> this.SortColumn(clickedCol,isAsc)),
-					})
-				);
+		private static string StripArrows (string columnName)
+		{
+			return columnName.Replace (" (▼)", string.Empty).Replace (" (▲)", string.Empty);
+		}
 
-				contextMenu.Show ();
+		private void ShowCellContextMenu (Point? clickedCell, MouseEventEventArgs e)
+		{
+			if (clickedCell == null) {
+				return;
 			}
 
-			private void ShowCellContextMenu (Point? clickedCell, MouseEventEventArgs e)
-			{
-				if (clickedCell == null) {
-					return;
-				}
-
-				var contextMenu = new ContextMenu (
-					e.MouseEvent.X + 1,
-					e.MouseEvent.Y + 1,
-					new MenuBarItem (new MenuItem []
-					{
-						new MenuItem($"New", string.Empty, () => dlg.New()),
-						new MenuItem($"Rename",string.Empty, ()=>  dlg.Rename()),
-						new MenuItem($"Delete",string.Empty, ()=>  dlg.Delete()),
-					})
-				);
+			var contextMenu = new ContextMenu (
+				e.MouseEvent.X + 1,
+				e.MouseEvent.Y + 1,
+				new MenuBarItem (new MenuItem []
+				{
+					new MenuItem($"New", string.Empty, () => New()),
+					new MenuItem($"Rename",string.Empty, ()=>  Rename()),
+					new MenuItem($"Delete",string.Empty, ()=>  Delete()),
+				})
+			);
 
-				dlg.tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false);
+			tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false);
 
-				contextMenu.Show ();
-			}
+			contextMenu.Show ();
+		}
 
-			private void HideColumn (DataColumn clickedCol)
-			{
-				var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol);
-				style.Visible = false;
-				this.tableView.Update ();
-			}
+		private void HideColumn (int clickedCol)
+		{
+			var style = this.tableView.Style.GetOrCreateColumnStyle (clickedCol);
+			style.Visible = false;
+			this.tableView.Update ();
 		}
+
 		/// <summary>
 		/// State representing a recursive search from <see cref="FileDialogState.Directory"/>
 		/// downwards.
@@ -1651,7 +1464,7 @@ namespace Terminal.Gui {
 			/// <returns></returns>
 			internal bool Cancel ()
 			{
-				var alreadyCancelled = token.IsCancellationRequested || cancel; 
+				var alreadyCancelled = token.IsCancellationRequested || cancel;
 
 				cancel = true;
 				token.Cancel ();
@@ -1659,5 +1472,28 @@ namespace Terminal.Gui {
 				return !alreadyCancelled;
 			}
 		}
+		internal class FileDialogCollectionNavigator : CollectionNavigatorBase {
+			private FileDialog fileDialog;
+
+			public FileDialogCollectionNavigator (FileDialog fileDialog)
+			{
+				this.fileDialog = fileDialog;
+			}
+
+			protected override object ElementAt (int idx)
+			{
+				var val = FileDialogTableSource.GetRawColumnValue (fileDialog.tableView.SelectedColumn, fileDialog.State?.Children [idx]);
+				if (val == null) {
+					return string.Empty;
+				}
+
+				return val.ToString ().Trim ('.');
+			}
+
+			protected override int GetCollectionLength ()
+			{
+				return fileDialog.State?.Children.Length ?? 0;
+			}
+		}
 	}
 }

+ 75 - 0
Terminal.Gui/Views/FileDialogTableSource.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Linq;
+
+namespace Terminal.Gui {
+	internal class FileDialogTableSource : ITableSource {
+		readonly FileDialogStyle style;
+		readonly int currentSortColumn;
+		readonly bool currentSortIsAsc;
+		readonly FileDialog dlg;
+		readonly FileDialogState state;
+
+		public FileDialogTableSource (FileDialog dlg, FileDialogState state, FileDialogStyle style, int currentSortColumn, bool currentSortIsAsc)
+		{
+			this.style = style;
+			this.currentSortColumn = currentSortColumn;
+			this.currentSortIsAsc = currentSortIsAsc;
+			this.dlg = dlg;
+			this.state = state;
+		}
+
+		public object this [int row, int col] => GetColumnValue (col, state.Children [row]);
+
+		private object GetColumnValue (int col, FileSystemInfoStats stats)
+		{
+			switch (col) {
+			case 0:
+				var icon = stats.IsParent ? null : style.IconGetter?.Invoke (
+					new FileDialogIconGetterArgs(dlg,stats.FileSystemInfo, FileDialogIconGetterContext.Table));
+				return icon + (stats?.Name ?? string.Empty);
+			case 1:
+				return stats?.HumanReadableLength ?? string.Empty;
+			case 2:
+				if (stats == null || stats.IsParent || stats.LastWriteTime == null) {
+					return string.Empty;
+				}
+				return stats.LastWriteTime.Value.ToString (style.DateFormat);
+			case 3:
+				return stats?.Type ?? string.Empty;
+			default:
+				throw new ArgumentOutOfRangeException (nameof (col));
+			}
+		}
+
+		internal static object GetRawColumnValue (int col, FileSystemInfoStats stats)
+		{
+			switch (col) {
+			case 0: return stats.FileSystemInfo.Name;
+			case 1: return stats.MachineReadableLength;
+			case 2: return stats.LastWriteTime;
+			case 3: return stats.Type;
+			}
+
+			throw new ArgumentOutOfRangeException (nameof (col));
+		}
+		public int Rows => state.Children.Count ();
+
+		public int Columns => 4;
+
+		public string [] ColumnNames => new string []{
+			MaybeAddSortArrows(style.FilenameColumnName,0),
+			MaybeAddSortArrows(style.SizeColumnName,1),
+			MaybeAddSortArrows(style.ModifiedColumnName,2),
+			MaybeAddSortArrows(style.TypeColumnName,3)
+		};
+
+		private string MaybeAddSortArrows (string name, int idx)
+		{
+			if (idx == currentSortColumn) {
+				return name + (currentSortIsAsc ? " (▲)" : " (▼)");
+			}
+
+			return name;
+		}
+	}
+}

+ 4 - 4
Terminal.Gui/Views/FrameView.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Linq;
 using System.Text.Json.Serialization;
-using NStack;
+using System.Text;
 using static Terminal.Gui.ConfigurationManager;
 
 namespace Terminal.Gui {
@@ -16,7 +16,7 @@ namespace Terminal.Gui {
 		/// <param name="frame">Frame.</param>
 		/// <param name="title">Title.</param>
 		/// <param name="views">Views.</param>
-		public FrameView (Rect frame, ustring title = null, View [] views = null) : base (frame)
+		public FrameView (Rect frame, string title = null, View [] views = null) : base (frame)
 		{
 			SetInitialProperties (frame, title, views);
 		}
@@ -25,7 +25,7 @@ namespace Terminal.Gui {
 		/// Initializes a new instance of the <see cref="Gui.FrameView"/> class using <see cref="LayoutStyle.Computed"/> layout.
 		/// </summary>
 		/// <param name="title">Title.</param>
-		public FrameView (ustring title)
+		public FrameView (string title)
 		{
 			SetInitialProperties (Rect.Empty, title, null);
 		}
@@ -46,7 +46,7 @@ namespace Terminal.Gui {
 		[SerializableConfigurationProperty (Scope = typeof (ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))]
 		public static LineStyle DefaultBorderStyle { get; set; } = LineStyle.Single;
 
-		void SetInitialProperties (Rect frame, ustring title, View [] views = null)
+		void SetInitialProperties (Rect frame, string title, View [] views = null)
 		{
 			this.Title = title;
 			Border.Thickness = new Thickness (1);

+ 1 - 25
Terminal.Gui/Views/GraphView/Annotations.cs

@@ -1,6 +1,6 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -262,29 +262,5 @@ namespace Terminal.Gui {
 				yield return new LineF (Points [i], Points [i + 1]);
 			}
 		}
-
-		/// <summary>
-		/// Describes two points in graph space and a line between them
-		/// </summary>
-		public class LineF {
-			/// <summary>
-			/// The start of the line
-			/// </summary>
-			public PointF Start { get; }
-
-			/// <summary>
-			/// The end point of the line
-			/// </summary>
-			public PointF End { get; }
-
-			/// <summary>
-			/// Creates a new line between the points
-			/// </summary>
-			public LineF (PointF start, PointF end)
-			{
-				this.Start = start;
-				this.End = end;
-			}
-		}
 	}
 }

+ 9 - 8
Terminal.Gui/Views/GraphView/Axis.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Text;
 
 namespace Terminal.Gui {
 
@@ -167,7 +168,7 @@ namespace Terminal.Gui {
 		protected override void DrawAxisLine (GraphView graph, int x, int y)
 		{
 			graph.Move (x, y);
-			Application.Driver.AddRune (Application.Driver.HLine);
+			Application.Driver.AddRune (CM.Glyphs.HLine);
 		}
 
 		/// <summary>
@@ -215,9 +216,9 @@ namespace Terminal.Gui {
 			var y = GetAxisYPosition (graph);
 
 			graph.Move (screenPosition, y);
-			
+
 			// draw the tick on the axis
-			driver.AddRune (driver.TopTee);
+			Application.Driver.AddRune (CM.Glyphs.TopTee);
 
 			// and the label text
 			if (!string.IsNullOrWhiteSpace (text)) {
@@ -282,7 +283,7 @@ namespace Terminal.Gui {
 
 				// Label or no label definetly render it
 				yield return toRender;
-				
+
 
 				current.X += Increment;
 			}
@@ -352,7 +353,7 @@ namespace Terminal.Gui {
 		protected override void DrawAxisLine (GraphView graph, int x, int y)
 		{
 			graph.Move (x, y);
-			Application.Driver.AddRune (Application.Driver.VLine);
+			Application.Driver.AddRune (CM.Glyphs.VLine);
 		}
 
 		private int GetAxisYEnd (GraphView graph)
@@ -402,7 +403,7 @@ namespace Terminal.Gui {
 				for (int i = 0; i < toRender.Length; i++) {
 
 					graph.Move (0, startDrawingAtY + i);
-					Application.Driver.AddRune (toRender [i]);
+					Application.Driver.AddRune ((Rune)toRender [i]);
 				}
 
 			}
@@ -448,7 +449,7 @@ namespace Terminal.Gui {
 
 				// draw the axis symbol (and label if it has one)
 				yield return toRender;
-				
+
 
 				current.Y += Increment;
 			}
@@ -470,7 +471,7 @@ namespace Terminal.Gui {
 			graph.Move (x, screenPosition);
 
 			// draw the tick on the axis
-			Application.Driver.AddRune (Application.Driver.RightTee);
+			Application.Driver.AddRune (CM.Glyphs.RightTee);
 
 			// and the label text
 			if (!string.IsNullOrWhiteSpace (text)) {

+ 38 - 0
Terminal.Gui/Views/GraphView/BarSeriesBar.cs

@@ -0,0 +1,38 @@
+namespace Terminal.Gui; 
+
+/// <summary>
+/// A single bar in a <see cref="BarSeries"/>
+/// </summary>
+public class BarSeriesBar {
+
+	/// <summary>
+	/// Optional text that describes the bar.  This will be rendered on the corresponding
+	/// <see cref="Axis"/> unless <see cref="BarSeries.DrawLabels"/> is false
+	/// </summary>
+	public string Text { get; set; }
+
+	/// <summary>
+	/// The color and character that will be rendered in the console
+	/// when the bar extends over it
+	/// </summary>
+	public GraphCellToRender Fill { get; set; }
+
+	/// <summary>
+	/// The value in graph space X/Y (depending on <see cref="Orientation"/>) to which the bar extends.
+	/// </summary>
+	public float Value { get; }
+
+	/// <summary>
+	/// Creates a new instance of a single bar rendered in the given <paramref name="fill"/> that extends
+	/// out <paramref name="value"/> graph space units in the default <see cref="Orientation"/>
+	/// </summary>
+	/// <param name="text"></param>
+	/// <param name="fill"></param>
+	/// <param name="value"></param>
+	public BarSeriesBar (string text, GraphCellToRender fill, float value)
+	{
+		Text = text;
+		Fill = fill;
+		Value = value;
+	}
+}

+ 1 - 0
Terminal.Gui/Views/GraphView/GraphCellToRender.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>

+ 16 - 17
Terminal.Gui/Views/GraphView/GraphView.cs

@@ -1,4 +1,4 @@
-using NStack;
+using System.Text;
 using System;
 using System.Collections.Generic;
 using System.Linq;
@@ -79,13 +79,13 @@ namespace Terminal.Gui {
 			AddCommand (Command.ScrollRight, () => { Scroll (CellSize.X, 0); return true; });
 			AddCommand (Command.ScrollLeft, () => { Scroll (-CellSize.X, 0); return true; });
 			AddCommand (Command.PageUp, () => { PageUp (); return true; });
-			AddCommand (Command.PageDown, () => { PageDown(); return true; });
+			AddCommand (Command.PageDown, () => { PageDown (); return true; });
 
 			AddKeyBinding (Key.CursorRight, Command.ScrollRight);
 			AddKeyBinding (Key.CursorLeft, Command.ScrollLeft);
 			AddKeyBinding (Key.CursorUp, Command.ScrollUp);
 			AddKeyBinding (Key.CursorDown, Command.ScrollDown);
-			
+
 			// Not bound by default (preserves backwards compatibility)
 			//AddKeyBinding (Key.PageUp, Command.PageUp);
 			//AddKeyBinding (Key.PageDown, Command.PageDown);
@@ -108,10 +108,10 @@ namespace Terminal.Gui {
 		}
 
 		///<inheritdoc/>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
-			if(CellSize.X == 0 || CellSize.Y == 0) {
-				throw new Exception ($"{nameof(CellSize)} cannot be 0");
+			if (CellSize.X == 0 || CellSize.Y == 0) {
+				throw new Exception ($"{nameof (CellSize)} cannot be 0");
 			}
 
 			SetDriverColorToGraphColor ();
@@ -139,7 +139,7 @@ namespace Terminal.Gui {
 			}
 
 			// Draw 'before' annotations
-			foreach (var a in Annotations.ToArray().Where (a => a.BeforeSeries)) {
+			foreach (var a in Annotations.ToArray ().Where (a => a.BeforeSeries)) {
 				a.Render (this);
 			}
 
@@ -152,17 +152,17 @@ namespace Terminal.Gui {
 			AxisX.DrawAxisLabels (this);
 
 			// Draw a cross where the two axis cross
-			var axisIntersection = new Point(AxisY.GetAxisXPosition(this),AxisX.GetAxisYPosition(this));
+			var axisIntersection = new Point (AxisY.GetAxisXPosition (this), AxisX.GetAxisYPosition (this));
 
 			if (AxisX.Visible && AxisY.Visible) {
 				Move (axisIntersection.X, axisIntersection.Y);
-				AddRune (axisIntersection.X, axisIntersection.Y, '\u253C');
+				AddRune (axisIntersection.X, axisIntersection.Y, (Rune)'\u253C');
 			}
 
 			SetDriverColorToGraphColor ();
 
-			Rect drawBounds = new Rect((int)MarginLeft,0, graphScreenWidth, graphScreenHeight);
-			
+			Rect drawBounds = new Rect ((int)MarginLeft, 0, graphScreenWidth, graphScreenHeight);
+
 			RectangleF graphSpace = ScreenToGraphSpace (drawBounds);
 
 			foreach (var s in Series.ToArray ()) {
@@ -179,7 +179,6 @@ namespace Terminal.Gui {
 			foreach (var a in Annotations.ToArray ().Where (a => !a.BeforeSeries)) {
 				a.Render (this);
 			}
-
 		}
 
 		/// <summary>
@@ -214,7 +213,7 @@ namespace Terminal.Gui {
 		public RectangleF ScreenToGraphSpace (Rect screenArea)
 		{
 			// get position of the bottom left
-			var pos = ScreenToGraphSpace (screenArea.Left, screenArea.Bottom-1);
+			var pos = ScreenToGraphSpace (screenArea.Left, screenArea.Bottom - 1);
 
 			return new RectangleF (pos.X, pos.Y, screenArea.Width * CellSize.X, screenArea.Height * CellSize.Y);
 		}
@@ -248,7 +247,7 @@ namespace Terminal.Gui {
 		public override bool ProcessKey (KeyEvent keyEvent)
 		{
 			if (HasFocus && CanFocus) {
-				var result =  InvokeKeybindings (keyEvent);
+				var result = InvokeKeybindings (keyEvent);
 				if (result != null)
 					return (bool)result;
 			}
@@ -259,7 +258,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Scrolls the graph up 1 page
 		/// </summary>
-		public void PageUp()
+		public void PageUp ()
 		{
 			Scroll (0, CellSize.Y * Bounds.Height);
 		}
@@ -267,9 +266,9 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Scrolls the graph down 1 page
 		/// </summary>
-		public void PageDown()
+		public void PageDown ()
 		{
-			Scroll(0, -1 * CellSize.Y * Bounds.Height);
+			Scroll (0, -1 * CellSize.Y * Bounds.Height);
 		}
 		/// <summary>
 		/// Scrolls the view by a given number of units in graph space.

+ 25 - 0
Terminal.Gui/Views/GraphView/LineF.cs

@@ -0,0 +1,25 @@
+namespace Terminal.Gui; 
+
+/// <summary>
+/// Describes two points in graph space and a line between them
+/// </summary>
+public class LineF {
+	/// <summary>
+	/// The start of the line
+	/// </summary>
+	public PointF Start { get; }
+
+	/// <summary>
+	/// The end point of the line
+	/// </summary>
+	public PointF End { get; }
+
+	/// <summary>
+	/// Creates a new line between the points
+	/// </summary>
+	public LineF (PointF start, PointF end)
+	{
+		this.Start = start;
+		this.End = end;
+	}
+}

+ 8 - 44
Terminal.Gui/Views/GraphView/Series.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Linq;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -32,9 +33,9 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// The color and character that will be rendered in the console
 		/// when there are point(s) in the corresponding graph space.
-		/// Defaults to uncolored 'x'
+		/// Defaults to uncolored 'dot'
 		/// </summary>
-		public GraphCellToRender Fill { get; set; } = new GraphCellToRender ('x');
+		public GraphCellToRender Fill { get; set; } = new GraphCellToRender (CM.Glyphs.Dot);
 
 		/// <summary>
 		/// Draws all points directly onto the graph
@@ -118,7 +119,7 @@ namespace Terminal.Gui {
 			}
 
 			for (int i = 0; i < values.Length; i++) {
-				subSeries [i].Bars.Add (new BarSeries.Bar (label,
+				subSeries [i].Bars.Add (new BarSeriesBar (label,
 					new GraphCellToRender (fill), values [i]));
 			}
 		}
@@ -146,7 +147,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Ordered collection of graph bars to position along axis
 		/// </summary>
-		public List<Bar> Bars { get; set; } = new List<Bar> ();
+		public List<BarSeriesBar> Bars { get; set; } = new List<BarSeriesBar> ();
 
 		/// <summary>
 		/// Determines the spacing of bars along the axis. Defaults to 1 i.e. 
@@ -168,12 +169,12 @@ namespace Terminal.Gui {
 		public float Offset { get; set; } = 0;
 
 		/// <summary>
-		/// Overrides the <see cref="Bar.Fill"/> with a fixed color
+		/// Overrides the <see cref="BarSeriesBar.Fill"/> with a fixed color
 		/// </summary>
 		public Attribute? OverrideBarColor { get; set; }
 
 		/// <summary>
-		/// True to draw <see cref="Bar.Text"/> along the axis under the bar.  Defaults
+		/// True to draw <see cref="BarSeriesBar.Text"/> along the axis under the bar.  Defaults
 		/// to true.
 		/// </summary>
 		public bool DrawLabels { get; set; } = true;
@@ -267,7 +268,7 @@ namespace Terminal.Gui {
 		/// <param name="start">Screen position of the start of the bar</param>
 		/// <param name="end">Screen position of the end of the bar</param>
 		/// <param name="beingDrawn">The Bar that occupies this space and is being drawn</param>
-		protected virtual void DrawBarLine (GraphView graph, Point start, Point end, Bar beingDrawn)
+		protected virtual void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn)
 		{
 			var adjusted = AdjustColor (beingDrawn.Fill);
 
@@ -279,42 +280,5 @@ namespace Terminal.Gui {
 
 			graph.SetDriverColorToGraphColor ();
 		}
-
-		/// <summary>
-		/// A single bar in a <see cref="BarSeries"/>
-		/// </summary>
-		public class Bar {
-
-			/// <summary>
-			/// Optional text that describes the bar.  This will be rendered on the corresponding
-			/// <see cref="Axis"/> unless <see cref="DrawLabels"/> is false
-			/// </summary>
-			public string Text { get; set; }
-
-			/// <summary>
-			/// The color and character that will be rendered in the console
-			/// when the bar extends over it
-			/// </summary>
-			public GraphCellToRender Fill { get; set; }
-
-			/// <summary>
-			/// The value in graph space X/Y (depending on <see cref="Orientation"/>) to which the bar extends.
-			/// </summary>
-			public float Value { get; }
-
-			/// <summary>
-			/// Creates a new instance of a single bar rendered in the given <paramref name="fill"/> that extends
-			/// out <paramref name="value"/> graph space units in the default <see cref="Orientation"/>
-			/// </summary>
-			/// <param name="text"></param>
-			/// <param name="fill"></param>
-			/// <param name="value"></param>
-			public Bar (string text, GraphCellToRender fill, float value)
-			{
-				Text = text;
-				Fill = fill;
-				Value = value;
-			}
-		}
 	}
 }

+ 8 - 7
Terminal.Gui/Views/HexView.cs

@@ -8,6 +8,7 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -198,7 +199,7 @@ namespace Terminal.Gui {
 		}
 
 		///<inheritdoc/>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
 			Attribute currentAttribute;
 			var current = ColorScheme.Focus;
@@ -217,7 +218,7 @@ namespace Terminal.Gui {
 
 			for (int line = 0; line < frame.Height; line++) {
 				var lineRect = new Rect (0, line, frame.Width, 1);
-				if (!bounds.Contains (lineRect))
+				if (!Bounds.Contains (lineRect))
 					continue;
 
 				Move (0, line);
@@ -238,7 +239,7 @@ namespace Terminal.Gui {
 
 						Driver.AddStr (offset >= n && !edited ? "  " : string.Format ("{0:x2}", value));
 						SetAttribute (GetNormalColor ());
-						Driver.AddRune (' ');
+						Driver.AddRune ((Rune)' ');
 					}
 					Driver.AddStr (block + 1 == nblocks ? " " : "| ");
 				}
@@ -248,14 +249,14 @@ namespace Terminal.Gui {
 					var b = GetData (data, offset, out bool edited);
 					Rune c;
 					if (offset >= n && !edited)
-						c = ' ';
+						c = (Rune)' ';
 					else {
 						if (b < 32)
-							c = '.';
+							c = (Rune)'.';
 						else if (b > 127)
-							c = '.';
+							c = (Rune)'.';
 						else
-							c = b;
+							Rune.DecodeFromUtf8 (new ReadOnlySpan<byte> (b), out c, out _);
 					}
 					if (offset + displayStart == position || edited)
 						SetAttribute (leftSide ? trackingColor : activeColor);

+ 1 - 1
Terminal.Gui/Views/HistoryTextItem.cs

@@ -1,7 +1,7 @@
 // TextView.cs: multi-line text editing
 using System;
 using System.Collections.Generic;
-using Rune = System.Rune;
+using System.Text;
 
 namespace Terminal.Gui {
 	partial class HistoryText {

+ 14 - 0
Terminal.Gui/Views/ITreeViewFilter.cs

@@ -0,0 +1,14 @@
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Provides filtering for a <see cref="TreeView"/>.
+	/// </summary>
+	public interface ITreeViewFilter<T> where T : class {
+
+		/// <summary>
+		/// Return <see langword="true"/> if the <paramref name="model"/> should
+		/// be included in the tree.
+		/// </summary>
+		bool IsMatch (T model);
+	}
+}

+ 5 - 5
Terminal.Gui/Views/Label.cs

@@ -6,7 +6,7 @@
 //
 
 using System;
-using NStack;
+using System.Text;
 
 namespace Terminal.Gui {
 	/// <summary>
@@ -30,25 +30,25 @@ namespace Terminal.Gui {
 		}
 
 		/// <inheritdoc/>
-		public Label (ustring text, bool autosize = true) : base (text)
+		public Label (string text, bool autosize = true) : base (text)
 		{
 			SetInitialProperties (autosize);
 		}
 
 		/// <inheritdoc/>
-		public Label (Rect rect, ustring text, bool autosize = false) : base (rect, text)
+		public Label (Rect rect, string text, bool autosize = false) : base (rect, text)
 		{
 			SetInitialProperties (autosize);
 		}
 
 		/// <inheritdoc/>
-		public Label (int x, int y, ustring text, bool autosize = true) : base (x, y, text)
+		public Label (int x, int y, string text, bool autosize = true) : base (x, y, text)
 		{
 			SetInitialProperties (autosize);
 		}
 
 		/// <inheritdoc/>
-		public Label (ustring text, TextDirection direction, bool autosize = true)
+		public Label (string text, TextDirection direction, bool autosize = true)
 			: base (text, direction)
 		{
 			SetInitialProperties (autosize);

+ 5 - 6
Terminal.Gui/Views/Line.cs

@@ -17,13 +17,13 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Constructs a Line object.
 		/// </summary>
-		public Line () 
+		public Line ()
 		{
 
 		}
-		
+
 		/// <inheritdoc/>
-		public override bool OnDrawFrames()
+		public override bool OnDrawFrames ()
 		{
 			var screenBounds = ViewToScreen (Bounds);
 			LineCanvas lc;
@@ -34,17 +34,16 @@ namespace Terminal.Gui {
 			return true;
 		}
 
-		//public override void OnDrawContentComplete (Rect viewport)
+		//public override void OnDrawContentComplete (Rect contentArea)
 		//{
 		//	var screenBounds = ViewToScreen (Frame);
 
 		//}
 
 		/// <inheritdoc/>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
 			OnDrawFrames ();
-
 		}
 	}
 }

+ 16 - 17
Terminal.Gui/Views/LineView.cs

@@ -1,7 +1,8 @@
 using System;
+using System.Text;
 
 namespace Terminal.Gui {
-	
+
 	/// <summary>
 	/// A straight line control either horizontal or vertical
 	/// </summary>
@@ -33,7 +34,7 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Creates a horizontal line
 		/// </summary>
-		public LineView () : this(Orientation.Horizontal)
+		public LineView () : this (Orientation.Horizontal)
 		{
 
 		}
@@ -49,13 +50,13 @@ namespace Terminal.Gui {
 			case Orientation.Horizontal:
 				Height = 1;
 				Width = Dim.Fill ();
-				LineRune = Driver.HLine;
+				LineRune = CM.Glyphs.HLine;
 
 				break;
 			case Orientation.Vertical:
 				Height = Dim.Fill ();
 				Width = 1;
-				LineRune = Driver.VLine;
+				LineRune = CM.Glyphs.VLine;
 				break;
 			default:
 				throw new ArgumentException ($"Unknown Orientation {orientation}");
@@ -66,32 +67,30 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Draws the line including any starting/ending anchors
 		/// </summary>
-		/// <param name="bounds"></param>
-		public override void Redraw (Rect bounds)
+		public override void OnDrawContent (Rect contentArea)
 		{
-			base.Redraw (bounds);
-			
+			base.OnDrawContent (contentArea);
+
 			Move (0, 0);
 			Driver.SetAttribute (GetNormalColor ());
 
-			var hLineWidth = Math.Max (1, Rune.ColumnWidth (Driver.HLine));
+			var hLineWidth = Math.Max (1, CM.Glyphs.HLine.GetColumns ());
 
 			var dEnd = Orientation == Orientation.Horizontal ?
-				bounds.Width :
-				bounds.Height;
+				Bounds.Width :
+				Bounds.Height;
 
 			for (int d = 0; d < dEnd; d += hLineWidth) {
-				
-				if(Orientation == Orientation.Horizontal) {
+
+				if (Orientation == Orientation.Horizontal) {
 					Move (d, 0);
-				}
-				else {
-					Move (0,d);
+				} else {
+					Move (0, d);
 				}
 
 				Rune rune = LineRune;
 
-				if(d == 0) {
+				if (d == 0) {
 					rune = StartingAnchor ?? LineRune;
 				} else
 				if (d == dEnd - 1) {

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