Browse Source

Merge branch 'develop' into v2_config_manager

Tig Kindel 2 years ago
parent
commit
1079793e9e
36 changed files with 2910 additions and 475 deletions
  1. 24 20
      .devcontainer/devcontainer.json
  2. 5 5
      Example/Example.csproj
  3. 0 4
      README.md
  4. 4 4
      ReactiveExample/ReactiveExample.csproj
  5. 66 61
      Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs
  6. 45 31
      Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs
  7. 42 26
      Terminal.Gui/ConsoleDrivers/NetDriver.cs
  8. 59 36
      Terminal.Gui/ConsoleDrivers/WindowsDriver.cs
  9. 24 22
      Terminal.Gui/Core/Application.cs
  10. 45 22
      Terminal.Gui/Core/Autocomplete/Autocomplete.cs
  11. 2 1
      Terminal.Gui/Core/Autocomplete/IAutocomplete.cs
  12. 304 66
      Terminal.Gui/Core/ConsoleDriver.cs
  13. 605 0
      Terminal.Gui/Core/Graphs/LineCanvas.cs
  14. 12 1
      Terminal.Gui/Core/Toplevel.cs
  15. 43 16
      Terminal.Gui/Core/View.cs
  16. 5 5
      Terminal.Gui/Terminal.Gui.csproj
  17. 5 5
      Terminal.Gui/Views/TextField.cs
  18. 23 15
      Terminal.Gui/Views/TextView.cs
  19. 234 0
      UICatalog/Scenarios/Animation.cs
  20. 3 2
      UICatalog/Scenarios/BasicColors.cs
  21. 212 0
      UICatalog/Scenarios/LineDrawing.cs
  22. BIN
      UICatalog/Scenarios/Spinning_globe_dark_small.gif
  23. 3 0
      UICatalog/Scenarios/spinning-globe-attribution.txt
  24. 1 1
      UICatalog/UICatalog.cs
  25. 8 4
      UICatalog/UICatalog.csproj
  26. 0 1
      UnitTests/Application/ApplicationTests.cs
  27. 346 0
      UnitTests/Core/LineCanvasTests.cs
  28. 69 5
      UnitTests/Drivers/AttributeTests.cs
  29. 38 0
      UnitTests/Drivers/ColorTests.cs
  30. 118 111
      UnitTests/Drivers/ConsoleDriverTests.cs
  31. 1 0
      UnitTests/Drivers/KeyTests.cs
  32. 42 5
      UnitTests/TopLevels/ToplevelTests.cs
  33. 86 0
      UnitTests/Views/AutocompleteTests.cs
  34. 2 2
      UnitTests/Views/TabViewTests.cs
  35. 118 1
      UnitTests/Views/TextFieldTests.cs
  36. 316 3
      UnitTests/Views/ViewTests.cs

+ 24 - 20
.devcontainer/devcontainer.json

@@ -1,25 +1,29 @@
 {
 	"name": "Terminal.Gui Codespace",
-	"image": "mcr.microsoft.com/vscode/devcontainers/dotnet:6.0",
-	"settings": {
-		"terminal.integrated.defaultProfile.linux": "pwsh"
-	},
-	"extensions": [
-		"eamodio.gitlens",
-		"ms-dotnettools.csharp",
-		"VisualStudioExptTeam.vscodeintellicode",
-		"ms-vscode.powershell",
-		"cschleiden.vscode-github-actions",
-		"redhat.vscode-yaml",
-		"bierner.markdown-preview-github-styles",
-		"ban.spellright",
-		"jmrog.vscode-nuget-package-manager",
-		"coenraads.bracket-pair-colorizer",
-		"vscode-icons-team.vscode-icons",
-		"editorconfig.editorconfig",
-		"formulahendry.dotnet-test-explorer"
-	],
-	"postCreateCommand": "dotnet restore && dotnet clean && dotnet build --configuration Release --no-restore && dotnet test --configuration Debug --no-restore --verbosity normal --collect:'XPlat Code Coverage' --settings UnitTests/coverlet.runsettings"
+	"image": "mcr.microsoft.com/vscode/devcontainers/dotnet:7.0",
+	"customizations": {
+		"vscode": {
+			"settings": {
+				"terminal.integrated.defaultProfile.linux": "pwsh"
+			},
+			"extensions": [
+				"eamodio.gitlens",
+				"ms-dotnettools.csharp",
+				"VisualStudioExptTeam.vscodeintellicode",
+				"ms-vscode.powershell",
+				"cschleiden.vscode-github-actions",
+				"redhat.vscode-yaml",
+				"bierner.markdown-preview-github-styles",
+				"ban.spellright",
+				"jmrog.vscode-nuget-package-manager",
+				"coenraads.bracket-pair-colorizer",
+				"vscode-icons-team.vscode-icons",
+				"editorconfig.editorconfig",
+				"formulahendry.dotnet-test-explorer"
+			],
+			"postCreateCommand": "dotnet restore && dotnet clean && dotnet build --configuration Release --no-restore && dotnet test --configuration Debug --no-restore --verbosity normal --collect:'XPlat Code Coverage' --settings UnitTests/coverlet.runsettings"		
+		}
+	}
 }
 
 // Built with ❤ by [Pipeline Foundation](https://pipeline.foundation)

+ 5 - 5
Example/Example.csproj

@@ -3,12 +3,12 @@
     <OutputType>Exe</OutputType>
     <TargetFramework>net6.0</TargetFramework>
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
-    <!-- In the source tree the version will always be 2.0 for all projects. -->
+    <!-- In the source tree the version will always be 1.0 for all projects. -->
     <!-- Do not modify these. -->
-    <AssemblyVersion>2.0</AssemblyVersion>
-    <FileVersion>2.0</FileVersion>
-    <Version>2.0</Version>
-    <InformationalVersion>2.0</InformationalVersion>
+    <AssemblyVersion>1.0</AssemblyVersion>
+    <FileVersion>1.0</FileVersion>
+    <Version>1.0</Version>
+    <InformationalVersion>1.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
     <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />

+ 0 - 4
README.md

@@ -6,10 +6,6 @@
 [![License](https://img.shields.io/github/license/gui-cs/gui.cs.svg)](LICENSE)
 ![Bugs](https://img.shields.io/github/issues/gui-cs/gui.cs/bug)
 
-# This is the v2.0 Branch - Under Development
-
-See the v2 Discussion here: https://github.com/gui-cs/Terminal.Gui/discussions/1940
-
 # Terminal.Gui - Cross Platform Terminal UI toolkit for .NET
 
 A toolkit for building rich console apps for .NET, .NET Core, and Mono that works on Windows, the Mac, and Linux/Unix.

+ 4 - 4
ReactiveExample/ReactiveExample.csproj

@@ -5,10 +5,10 @@
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
     <!-- In the source tree the version will always be 2.0 for all projects. -->
     <!-- Do not modify these. -->
-    <AssemblyVersion>2.0</AssemblyVersion>
-    <FileVersion>2.0</FileVersion>
-    <Version>2.0</Version>
-    <InformationalVersion>2.0</InformationalVersion>
+    <AssemblyVersion>1.0</AssemblyVersion>
+    <FileVersion>1.0</FileVersion>
+    <Version>1.0</Version>
+    <InformationalVersion>1.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="ReactiveUI.Fody" Version="18.4.1" />

+ 66 - 61
Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs

@@ -62,50 +62,72 @@ namespace Terminal.Gui {
 					Curses.move (crow, ccol);
 					needMove = false;
 				}
-				if (runeWidth < 2 && ccol > 0
-					&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
-
-					var curAtttib = currentAttribute;
-					Curses.attrset (contents [crow, ccol - 1, 1]);
-					Curses.mvaddch (crow, ccol - 1, (int)(uint)' ');
-					contents [crow, ccol - 1, 0] = (int)(uint)' ';
-					Curses.move (crow, ccol);
-					Curses.attrset (curAtttib);
-
-				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-					&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
-
-					var curAtttib = currentAttribute;
-					Curses.attrset (contents [crow, ccol + 1, 1]);
-					Curses.mvaddch (crow, ccol + 1, (int)(uint)' ');
-					contents [crow, ccol + 1, 0] = (int)(uint)' ';
-					Curses.move (crow, ccol);
-					Curses.attrset (curAtttib);
+				if (runeWidth == 0 && ccol > 0) {
+					var r = contents [crow, ccol - 1, 0];
+					var s = new string (new char [] { (char)r, (char)rune });
+					string sn;
+					if (!s.IsNormalized ()) {
+						sn = s.Normalize ();
+					} else {
+						sn = s;
+					}
+					var c = sn [0];
+					Curses.mvaddch (crow, ccol - 1, (int)(uint)c);
+					contents [crow, ccol - 1, 0] = c;
+					contents [crow, ccol - 1, 1] = CurrentAttribute;
+					contents [crow, ccol - 1, 2] = 1;
 
-				}
-				if (runeWidth > 1 && ccol == Clip.Right - 1) {
-					Curses.addch ((int)(uint)' ');
-					contents [crow, ccol, 0] = (int)(uint)' ';
 				} else {
-					Curses.addch ((int)(uint)rune);
-					contents [crow, ccol, 0] = (int)(uint)rune;
+					if (runeWidth < 2 && ccol > 0
+						&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
+
+						var curAtttib = CurrentAttribute;
+						Curses.attrset (contents [crow, ccol - 1, 1]);
+						Curses.mvaddch (crow, ccol - 1, (int)(uint)' ');
+						contents [crow, ccol - 1, 0] = (int)(uint)' ';
+						Curses.move (crow, ccol);
+						Curses.attrset (curAtttib);
+
+					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
+						&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+
+						var curAtttib = CurrentAttribute;
+						Curses.attrset (contents [crow, ccol + 1, 1]);
+						Curses.mvaddch (crow, ccol + 1, (int)(uint)' ');
+						contents [crow, ccol + 1, 0] = (int)(uint)' ';
+						Curses.move (crow, ccol);
+						Curses.attrset (curAtttib);
+
+					}
+					if (runeWidth > 1 && ccol == Clip.Right - 1) {
+						Curses.addch ((int)(uint)' ');
+						contents [crow, ccol, 0] = (int)(uint)' ';
+					} else {
+						Curses.addch ((int)(uint)rune);
+						contents [crow, ccol, 0] = (int)(uint)rune;
+					}
+					contents [crow, ccol, 1] = CurrentAttribute;
+					contents [crow, ccol, 2] = 1;
 				}
-				contents [crow, ccol, 1] = currentAttribute;
-				contents [crow, ccol, 2] = 1;
-			} else
+			} else {
 				needMove = true;
+			}
 
-			ccol++;
+			if (runeWidth < 0 || runeWidth > 0) {
+				ccol++;
+			}
+			
 			if (runeWidth > 1) {
 				if (validClip && ccol < Clip.Right) {
-					contents [crow, ccol, 1] = currentAttribute;
+					contents [crow, ccol, 1] = CurrentAttribute;
 					contents [crow, ccol, 2] = 0;
 				}
 				ccol++;
 			}
 
-			if (sync)
+			if (sync) {
 				UpdateScreen ();
+			}
 		}
 
 		public override void AddStr (ustring str)
@@ -160,12 +182,10 @@ namespace Terminal.Gui {
 
 		public override void UpdateScreen () => window.redrawwin ();
 
-		Attribute currentAttribute;
-
 		public override void SetAttribute (Attribute c)
 		{
-			currentAttribute = c;
-			Curses.attrset (currentAttribute);
+			base.SetAttribute (c);
+			Curses.attrset (CurrentAttribute);
 		}
 
 		public Curses.Window window;
@@ -201,6 +221,7 @@ namespace Terminal.Gui {
 
 		public override void SetColors (ConsoleColor foreground, ConsoleColor background)
 		{
+			// BUGBUG: This code is never called ?? See Issue #2300
 			int f = (short)foreground;
 			int b = (short)background;
 			var v = colorPairs [f, b];
@@ -218,6 +239,7 @@ namespace Terminal.Gui {
 		Dictionary<int, int> rawPairs = new Dictionary<int, int> ();
 		public override void SetColors (short foreColorId, short backgroundColorId)
 		{
+			// BUGBUG: This code is never called ?? See Issue #2300
 			int key = ((ushort)foreColorId << 16) | (ushort)backgroundColorId;
 			if (!rawPairs.TryGetValue (key, out var v)) {
 				v = MakeColor (foreColorId, backgroundColorId);
@@ -875,34 +897,18 @@ namespace Terminal.Gui {
 			if (reportableMouseEvents.HasFlag (Curses.Event.ReportMousePosition))
 				StartReportingMouseMoves ();
 
-			ResizeScreen ();
-			UpdateOffScreen ();
-
-			//HLine = Curses.ACS_HLINE;
-			//VLine = Curses.ACS_VLINE;
-			//Stipple = Curses.ACS_CKBOARD;
-			//Diamond = Curses.ACS_DIAMOND;
-			//ULCorner = Curses.ACS_ULCORNER;
-			//LLCorner = Curses.ACS_LLCORNER;
-			//URCorner = Curses.ACS_URCORNER;
-			//LRCorner = Curses.ACS_LRCORNER;
-			//LeftTee = Curses.ACS_LTEE;
-			//RightTee = Curses.ACS_RTEE;
-			//TopTee = Curses.ACS_TTEE;
-			//BottomTee = Curses.ACS_BTEE;
-			//RightArrow = Curses.ACS_RARROW;
-			//LeftArrow = Curses.ACS_LARROW;
-			//UpArrow = Curses.ACS_UARROW;
-			//DownArrow = Curses.ACS_DARROW;
+			CurrentAttribute = MakeColor (Color.White, Color.Black);
 
 			if (Curses.HasColors) {
 				Curses.StartColor ();
 				Curses.UseDefaultColors ();
 
-				CreateColors ();
+				InitalizeColorSchemes ();
 			} else {
-				CreateColors (false);
+				InitalizeColorSchemes (false);
 
+				// BUGBUG: This is a hack to make the colors work on the Mac?
+				// The new Theme support overwrites these colors, so this is not needed?
 				Colors.TopLevel.Normal = Curses.COLOR_GREEN;
 				Colors.TopLevel.Focus = Curses.COLOR_WHITE;
 				Colors.TopLevel.HotNormal = Curses.COLOR_YELLOW;
@@ -929,6 +935,10 @@ namespace Terminal.Gui {
 				Colors.Error.HotFocus = Curses.A_REVERSE;
 				Colors.Error.Disabled = Curses.A_BOLD | Curses.COLOR_GRAY;
 			}
+
+			ResizeScreen ();
+			UpdateOffScreen ();
+
 		}
 
 		public override void ResizeScreen ()
@@ -1093,11 +1103,6 @@ namespace Terminal.Gui {
 			//Curses.mouseinterval (lastMouseInterval);
 		}
 
-		public override Attribute GetAttribute ()
-		{
-			return currentAttribute;
-		}
-
 		/// <inheritdoc/>
 		public override bool GetCursorVisibility (out CursorVisibility visibility)
 		{

+ 45 - 31
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -33,7 +33,7 @@ namespace Terminal.Gui {
 				UseFakeClipboard = useFakeClipboard;
 				FakeClipboardAlwaysThrowsNotSupportedException = fakeClipboardAlwaysThrowsNotSupportedException;
 				FakeClipboardIsSupportedAlwaysFalse = fakeClipboardIsSupportedAlwaysTrue;
-				
+
 				// double check usage is correct
 				Debug.Assert (useFakeClipboard == false && fakeClipboardAlwaysThrowsNotSupportedException == false);
 				Debug.Assert (useFakeClipboard == false && fakeClipboardIsSupportedAlwaysTrue == false);
@@ -131,34 +131,54 @@ namespace Terminal.Gui {
 					//MockConsole.CursorTop = crow;
 					needMove = false;
 				}
-				if (runeWidth < 2 && ccol > 0
+				if (runeWidth == 0 && ccol > 0) {
+					var r = contents [crow, ccol - 1, 0];
+					var s = new string (new char [] { (char)r, (char)rune });
+					string sn;
+					if (!s.IsNormalized ()) {
+						sn = s.Normalize ();
+					} else {
+						sn = s;
+					}
+					var c = sn [0];
+					contents [crow, ccol - 1, 0] = c;
+					contents [crow, ccol - 1, 1] = CurrentAttribute;
+					contents [crow, ccol - 1, 2] = 1;
+
+				} else {
+					if (runeWidth < 2 && ccol > 0
 					&& Rune.ColumnWidth ((Rune)contents [crow, ccol - 1, 0]) > 1) {
 
-					contents [crow, ccol - 1, 0] = (int)(uint)' ';
+						contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
-				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-					&& Rune.ColumnWidth ((Rune)contents [crow, ccol, 0]) > 1) {
+					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
+						&& Rune.ColumnWidth ((Rune)contents [crow, ccol, 0]) > 1) {
 
-					contents [crow, ccol + 1, 0] = (int)(uint)' ';
-					contents [crow, ccol + 1, 2] = 1;
+						contents [crow, ccol + 1, 0] = (int)(uint)' ';
+						contents [crow, ccol + 1, 2] = 1;
 
-				}
-				if (runeWidth > 1 && ccol == Clip.Right - 1) {
-					contents [crow, ccol, 0] = (int)(uint)' ';
-				} else {
-					contents [crow, ccol, 0] = (int)(uint)rune;
-				}
-				contents [crow, ccol, 1] = currentAttribute;
-				contents [crow, ccol, 2] = 1;
+					}
+					if (runeWidth > 1 && ccol == Clip.Right - 1) {
+						contents [crow, ccol, 0] = (int)(uint)' ';
+					} else {
+						contents [crow, ccol, 0] = (int)(uint)rune;
+					}
+					contents [crow, ccol, 1] = CurrentAttribute;
+					contents [crow, ccol, 2] = 1;
 
-				dirtyLine [crow] = true;
-			} else
+					dirtyLine [crow] = true;
+				}
+			} else {
 				needMove = true;
+			}
+
+			if (runeWidth < 0 || runeWidth > 0) {
+				ccol++;
+			}
 
-			ccol++;
 			if (runeWidth > 1) {
 				if (validClip && ccol < Clip.Right) {
-					contents [crow, ccol, 1] = currentAttribute;
+					contents [crow, ccol, 1] = CurrentAttribute;
 					contents [crow, ccol, 2] = 0;
 				}
 				ccol++;
@@ -169,8 +189,9 @@ namespace Terminal.Gui {
 			//	if (crow + 1 < Rows)
 			//		crow++;
 			//}
-			if (sync)
+			if (sync) {
 				UpdateScreen ();
+			}
 		}
 
 		public override void AddStr (ustring str)
@@ -208,11 +229,10 @@ namespace Terminal.Gui {
 			rows = FakeConsole.WindowHeight = FakeConsole.BufferHeight = FakeConsole.HEIGHT;
 			FakeConsole.Clear ();
 			ResizeScreen ();
+			// Call InitalizeColorSchemes before UpdateOffScreen as it references Colors
+			CurrentAttribute = MakeColor (Color.White, Color.Black);
+			InitalizeColorSchemes ();
 			UpdateOffScreen ();
-
-			CreateColors ();
-
-			//MockConsole.Clear ();
 		}
 
 		public override Attribute MakeAttribute (Color fore, Color back)
@@ -283,10 +303,9 @@ namespace Terminal.Gui {
 			UpdateCursor ();
 		}
 
-		Attribute currentAttribute;
 		public override void SetAttribute (Attribute c)
 		{
-			currentAttribute = c;
+			base.SetAttribute (c);
 		}
 
 		public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
@@ -475,11 +494,6 @@ namespace Terminal.Gui {
 			keyUpHandler (new KeyEvent (map, keyModifiers));
 		}
 
-		public override Attribute GetAttribute ()
-		{
-			return currentAttribute;
-		}
-
 		/// <inheritdoc/>
 		public override bool GetCursorVisibility (out CursorVisibility visibility)
 		{

+ 42 - 26
Terminal.Gui/ConsoleDrivers/NetDriver.cs

@@ -1241,33 +1241,52 @@ namespace Terminal.Gui {
 			var validClip = IsValidContent (ccol, crow, Clip);
 
 			if (validClip) {
-				if (runeWidth < 2 && ccol > 0
-					&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
+				if (runeWidth == 0 && ccol > 0) {
+					var r = contents [crow, ccol - 1, 0];
+					var s = new string (new char [] { (char)r, (char)rune });
+					string sn;
+					if (!s.IsNormalized ()) {
+						sn = s.Normalize ();
+					} else {
+						sn = s;
+					}
+					var c = sn [0];
+					contents [crow, ccol - 1, 0] = c;
+					contents [crow, ccol - 1, 1] = CurrentAttribute;
+					contents [crow, ccol - 1, 2] = 1;
 
-					contents [crow, ccol - 1, 0] = (int)(uint)' ';
+				} else {
+					if (runeWidth < 2 && ccol > 0
+						&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
 
-				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-					&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+						contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
-					contents [crow, ccol + 1, 0] = (int)(uint)' ';
-					contents [crow, ccol + 1, 2] = 1;
+					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
+						&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
 
-				}
-				if (runeWidth > 1 && ccol == Clip.Right - 1) {
-					contents [crow, ccol, 0] = (int)(uint)' ';
-				} else {
-					contents [crow, ccol, 0] = (int)(uint)rune;
-				}
-				contents [crow, ccol, 1] = currentAttribute;
-				contents [crow, ccol, 2] = 1;
+						contents [crow, ccol + 1, 0] = (int)(uint)' ';
+						contents [crow, ccol + 1, 2] = 1;
+
+					}
+					if (runeWidth > 1 && ccol == Clip.Right - 1) {
+						contents [crow, ccol, 0] = (int)(uint)' ';
+					} else {
+						contents [crow, ccol, 0] = (int)(uint)rune;
+					}
+					contents [crow, ccol, 1] = CurrentAttribute;
+					contents [crow, ccol, 2] = 1;
 
+				}
 				dirtyLine [crow] = true;
 			}
 
-			ccol++;
+			if (runeWidth < 0 || runeWidth > 0) {
+				ccol++;
+			}
+			
 			if (runeWidth > 1) {
 				if (validClip && ccol < Clip.Right) {
-					contents [crow, ccol, 1] = currentAttribute;
+					contents [crow, ccol, 1] = CurrentAttribute;
 					contents [crow, ccol, 2] = 0;
 				}
 				ccol++;
@@ -1340,12 +1359,14 @@ namespace Terminal.Gui {
 			cols = Console.WindowWidth;
 			rows = Console.WindowHeight;
 
+			CurrentAttribute = MakeColor (Color.White, Color.Black);
+			InitalizeColorSchemes ();
+
 			ResizeScreen ();
 			UpdateOffScreen ();
 
 			StartReportingMouseMoves ();
 
-			CreateColors ();
 
 			Clear ();
 		}
@@ -1485,7 +1506,7 @@ namespace Terminal.Gui {
 						outputWidth++;
 						var rune = contents [row, col, 0];
 						char [] spair;
-						if (Rune.DecodeSurrogatePair((uint) rune, out spair)) {
+						if (Rune.DecodeSurrogatePair ((uint)rune, out spair)) {
 							output.Append (spair);
 						} else {
 							output.Append ((char)rune);
@@ -1613,10 +1634,10 @@ namespace Terminal.Gui {
 		{
 		}
 
-		Attribute currentAttribute;
+
 		public override void SetAttribute (Attribute c)
 		{
-			currentAttribute = c;
+			base.SetAttribute (c);
 		}
 
 		public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
@@ -1934,11 +1955,6 @@ namespace Terminal.Gui {
 			};
 		}
 
-		public override Attribute GetAttribute ()
-		{
-			return currentAttribute;
-		}
-
 		/// <inheritdoc/>
 		public override bool GetCursorVisibility (out CursorVisibility visibility)
 		{

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

@@ -116,6 +116,10 @@ namespace Terminal.Gui {
 
 		public bool GetCursorVisibility (out CursorVisibility visibility)
 		{
+			if (ScreenBuffer == IntPtr.Zero) {
+				visibility = CursorVisibility.Invisible;
+				return false;
+			}
 			if (!GetConsoleCursorInfo (ScreenBuffer, out ConsoleCursorInfo info)) {
 				var err = Marshal.GetLastWin32Error ();
 				if (err != 0) {
@@ -283,6 +287,9 @@ namespace Terminal.Gui {
 			position = new Point (csbi.srWindow.Left, csbi.srWindow.Top);
 			SetConsoleOutputWindow (csbi);
 			var winRect = new SmallRect (0, 0, (short)(sz.Width - 1), (short)Math.Max (sz.Height - 1, 0));
+			if (!SetConsoleScreenBufferInfoEx (OutputHandle, ref csbi)) {
+				throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
+			}
 			if (!SetConsoleWindowInfo (OutputHandle, true, ref winRect)) {
 				throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
 			}
@@ -1455,13 +1462,13 @@ namespace Terminal.Gui {
 				var winSize = WinConsole.GetConsoleOutputWindow (out Point pos);
 				cols = winSize.Width;
 				rows = winSize.Height;
-
 				WindowsConsole.SmallRect.MakeEmpty (ref damageRegion);
 
+				CurrentAttribute = MakeColor (Color.White, Color.Black);
+				InitalizeColorSchemes ();
+
 				ResizeScreen ();
 				UpdateOffScreen ();
-
-				CreateColors ();
 			} catch (Win32Exception e) {
 				throw new InvalidOperationException ("The Windows Console output window is not available.", e);
 			}
@@ -1517,49 +1524,72 @@ namespace Terminal.Gui {
 			var validClip = IsValidContent (ccol, crow, Clip);
 
 			if (validClip) {
-				if (runeWidth < 2 && ccol > 0
-					&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
-
+				if (runeWidth == 0 && ccol > 0) {
+					var r = contents [crow, ccol - 1, 0];
+					var s = new string (new char [] { (char)r, (char)rune });
+					string sn;
+					if (!s.IsNormalized ()) {
+						sn = s.Normalize ();
+					} else {
+						sn = s;
+					}
+					var c = sn [0];
 					var prevPosition = crow * Cols + (ccol - 1);
-					OutputBuffer [prevPosition].Char.UnicodeChar = ' ';
-					contents [crow, ccol - 1, 0] = (int)(uint)' ';
+					OutputBuffer [prevPosition].Char.UnicodeChar = c;
+					contents [crow, ccol - 1, 0] = c;
+					OutputBuffer [prevPosition].Attributes = (ushort)CurrentAttribute;
+					contents [crow, ccol - 1, 1] = CurrentAttribute;
+					contents [crow, ccol - 1, 2] = 1;
+					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) {
 
-				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-					&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+						var prevPosition = crow * Cols + (ccol - 1);
+						OutputBuffer [prevPosition].Char.UnicodeChar = ' ';
+						contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
-					var prevPosition = GetOutputBufferPosition () + 1;
-					OutputBuffer [prevPosition].Char.UnicodeChar = (char)' ';
-					contents [crow, ccol + 1, 0] = (int)(uint)' ';
+					} else if (runeWidth < 2 && ccol <= Clip.Right - 1
+						&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
 
+						var prevPosition = GetOutputBufferPosition () + 1;
+						OutputBuffer [prevPosition].Char.UnicodeChar = (char)' ';
+						contents [crow, ccol + 1, 0] = (int)(uint)' ';
+
+					}
+					if (runeWidth > 1 && ccol == Clip.Right - 1) {
+						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].Attributes = (ushort)CurrentAttribute;
+					contents [crow, ccol, 1] = CurrentAttribute;
+					contents [crow, ccol, 2] = 1;
+					WindowsConsole.SmallRect.Update (ref damageRegion, (short)ccol, (short)crow);
 				}
-				if (runeWidth > 1 && ccol == Clip.Right - 1) {
-					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].Attributes = (ushort)currentAttribute;
-				contents [crow, ccol, 1] = currentAttribute;
-				contents [crow, ccol, 2] = 1;
-				WindowsConsole.SmallRect.Update (ref damageRegion, (short)ccol, (short)crow);
 			}
 
-			ccol++;
+			if (runeWidth < 0 || runeWidth > 0) {
+				ccol++;
+			}
+
 			if (runeWidth > 1) {
 				if (validClip && ccol < Clip.Right) {
 					position = GetOutputBufferPosition ();
-					OutputBuffer [position].Attributes = (ushort)currentAttribute;
+					OutputBuffer [position].Attributes = (ushort)CurrentAttribute;
 					OutputBuffer [position].Char.UnicodeChar = (char)0x00;
 					contents [crow, ccol, 0] = (int)(uint)0x00;
-					contents [crow, ccol, 1] = currentAttribute;
+					contents [crow, ccol, 1] = CurrentAttribute;
 					contents [crow, ccol, 2] = 0;
 				}
 				ccol++;
 			}
 
-			if (sync)
+			if (sync) {
 				UpdateScreen ();
+			}
 		}
 
 		public override void AddStr (ustring str)
@@ -1568,11 +1598,9 @@ namespace Terminal.Gui {
 				AddRune (rune);
 		}
 
-		Attribute currentAttribute;
-
 		public override void SetAttribute (Attribute c)
 		{
-			currentAttribute = c;
+			base.SetAttribute (c);
 		}
 
 		public override Attribute MakeColor (Color foreground, Color background)
@@ -1674,11 +1702,6 @@ namespace Terminal.Gui {
 			WinConsole = null;
 		}
 
-		public override Attribute GetAttribute ()
-		{
-			return currentAttribute;
-		}
-
 		/// <inheritdoc/>
 		public override bool GetCursorVisibility (out CursorVisibility visibility)
 		{

+ 24 - 22
Terminal.Gui/Core/Application.cs

@@ -57,7 +57,7 @@ namespace Terminal.Gui {
 	///   </para>
 	/// </remarks>
 	public static class Application {
-		static Stack<Toplevel> toplevels = new Stack<Toplevel> ();
+		static readonly Stack<Toplevel> toplevels = new Stack<Toplevel> ();
 
 		/// <summary>
 		/// The current <see cref="ConsoleDriver"/> in use.
@@ -111,28 +111,33 @@ namespace Terminal.Gui {
 		/// </summary>
 		public static View WantContinuousButtonPressedView { get; private set; }
 
+		private static bool? _heightAsBuffer;
+
 		/// <summary>
 		/// The current <see cref="ConsoleDriver.HeightAsBuffer"/> used in the terminal.
 		/// </summary>
+		/// 
 		public static bool HeightAsBuffer {
 			get {
 				if (Driver == null) {
-					throw new ArgumentNullException ("The driver must be initialized first.");
+					return _heightAsBuffer.HasValue && _heightAsBuffer.Value;
 				}
 				return Driver.HeightAsBuffer;
 			}
 			set {
+				_heightAsBuffer = value;
 				if (Driver == null) {
-					throw new ArgumentNullException ("The driver must be initialized first.");
+					return;
 				}
-				Driver.HeightAsBuffer = value;
+
+				Driver.HeightAsBuffer = _heightAsBuffer.Value;
 			}
 		}
 
 		static Key alternateForwardKey = Key.PageDown | Key.CtrlMask;
 
 		/// <summary>
-		/// Alternative key to navigate forwards through all views. Ctrl+Tab is always used.
+		/// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.
 		/// </summary>
 		public static Key AlternateForwardKey {
 			get => alternateForwardKey;
@@ -147,7 +152,7 @@ namespace Terminal.Gui {
 
 		static void OnAlternateForwardKeyChanged (Key oldKey)
 		{
-			foreach (var top in toplevels) {
+			foreach (var top in toplevels.ToArray()) {
 				top.OnAlternateForwardKeyChanged (oldKey);
 			}
 		}
@@ -155,7 +160,7 @@ namespace Terminal.Gui {
 		static Key alternateBackwardKey = Key.PageUp | Key.CtrlMask;
 
 		/// <summary>
-		/// Alternative key to navigate backwards through all views. Shift+Ctrl+Tab is always used.
+		/// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.
 		/// </summary>
 		public static Key AlternateBackwardKey {
 			get => alternateBackwardKey;
@@ -170,7 +175,7 @@ namespace Terminal.Gui {
 
 		static void OnAlternateBackwardKeyChanged (Key oldKey)
 		{
-			foreach (var top in toplevels) {
+			foreach (var top in toplevels.ToArray()) {
 				top.OnAlternateBackwardKeyChanged (oldKey);
 			}
 		}
@@ -200,7 +205,8 @@ namespace Terminal.Gui {
 
 		static void OnQuitKeyChanged (Key oldKey)
 		{
-			foreach (var top in toplevels) {
+			// Duplicate the list so if it changes during enumeration we're safe
+			foreach (var top in toplevels.ToArray()) {
 				top.OnQuitKeyChanged (oldKey);
 			}
 		}
@@ -212,7 +218,7 @@ namespace Terminal.Gui {
 		public static MainLoop MainLoop { get; private set; }
 
 		/// <summary>
-		/// Disable or enable the mouse in this <see cref="Application"/>
+		/// Disable or enable the mouse. The mouse is enabled by default.
 		/// </summary>
 		public static bool IsMouseDisabled { get; set; }
 
@@ -266,7 +272,7 @@ namespace Terminal.Gui {
 		// users use async/await on their code
 		//
 		class MainLoopSyncContext : SynchronizationContext {
-			MainLoop mainLoop;
+			readonly MainLoop mainLoop;
 
 			public MainLoopSyncContext (MainLoop mainLoop)
 			{
@@ -305,9 +311,9 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// If set, it forces the use of the System.Console-based driver.
+		/// If <see langword="true"/>, forces the use of the System.Console-based (see <see cref="NetDriver"/>) driver. The default is <see langword="false"/>.
 		/// </summary>
-		public static bool UseSystemConsole;
+		public static bool UseSystemConsole { get; set; } = false;
 
 		// For Unit testing - ignores UseSystemConsole
 		internal static bool ForceFakeConsole;
@@ -422,6 +428,7 @@ namespace Terminal.Gui {
 			MainLoop = new MainLoop (mainLoopDriver);
 
 			try {
+				Driver.HeightAsBuffer = HeightAsBuffer;
 				Driver.Init (TerminalResized);
 			} catch (InvalidOperationException ex) {
 				// This is a case where the driver is unable to initialize the console.
@@ -933,6 +940,8 @@ namespace Terminal.Gui {
 				if (Top != null && toplevel != Top && !toplevels.Contains (Top)) {
 					Top.Dispose ();
 					Top = null;
+				} else if (Top != null && toplevel != Top && toplevels.Contains (Top)) {
+					Top.OnLeave (toplevel);
 				}
 				if (string.IsNullOrEmpty (toplevel.Id.ToString ())) {
 					var count = 1;
@@ -986,9 +995,7 @@ namespace Terminal.Gui {
 			toplevel.PositionToplevels ();
 			toplevel.WillPresent ();
 			if (refreshDriver) {
-				if (MdiTop != null) {
-					MdiTop.OnChildLoaded (toplevel);
-				}
+				MdiTop?.OnChildLoaded (toplevel);
 				toplevel.OnLoaded ();
 				Redraw (toplevel);
 				toplevel.PositionCursor ();
@@ -1043,6 +1050,7 @@ namespace Terminal.Gui {
 					MdiTop.OnAllChildClosed ();
 				} else {
 					SetCurrentAsTop ();
+					Current.OnEnter (Current);
 				}
 				Refresh ();
 			}
@@ -1111,12 +1119,6 @@ namespace Terminal.Gui {
 			Driver.Refresh ();
 		}
 
-		static void Refresh (View view)
-		{
-			view.Redraw (view.Bounds);
-			Driver.Refresh ();
-		}
-
 		/// <summary>
 		/// Triggers a refresh of the entire display.
 		/// </summary>

+ 45 - 22
Terminal.Gui/Core/Autocomplete/Autocomplete.cs

@@ -324,6 +324,7 @@ namespace Terminal.Gui {
 			if (IsWordChar ((char)kb.Key)) {
 				Visible = true;
 				closed = false;
+				return false;
 			}
 
 			if (kb.Key == Reopen) {
@@ -332,6 +333,9 @@ namespace Terminal.Gui {
 
 			if (closed || Suggestions.Count == 0) {
 				Visible = false;
+				if (!closed) {
+					Close ();
+				}
 				return false;
 			}
 
@@ -345,6 +349,17 @@ namespace Terminal.Gui {
 				return true;
 			}
 
+			if (kb.Key == Key.CursorLeft || kb.Key == Key.CursorRight) {
+				GenerateSuggestions (kb.Key == Key.CursorLeft ? -1 : 1);
+				if (Suggestions.Count == 0) {
+					Visible = false;
+					if (!closed) {
+						Close ();
+					}
+				}
+				return false;
+			}
+
 			if (kb.Key == SelectionKey) {
 				return Select ();
 			}
@@ -368,6 +383,9 @@ namespace Terminal.Gui {
 		public virtual bool MouseEvent (MouseEvent me, bool fromHost = false)
 		{
 			if (fromHost) {
+				if (!Visible) {
+					return false;
+				}
 				GenerateSuggestions ();
 				if (Visible && Suggestions.Count == 0) {
 					Visible = false;
@@ -444,7 +462,8 @@ namespace Terminal.Gui {
 		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
 		/// match with the current cursor position/text in the <see cref="HostControl"/>
 		/// </summary>
-		public virtual void GenerateSuggestions ()
+		/// <param name="columnOffset">The column offset.</param>
+		public virtual void GenerateSuggestions (int columnOffset = 0)
 		{
 			// if there is nothing to pick from
 			if (AllSuggestions.Count == 0) {
@@ -452,7 +471,7 @@ namespace Terminal.Gui {
 				return;
 			}
 
-			var currentWord = GetCurrentWord ();
+			var currentWord = GetCurrentWord (columnOffset);
 
 			if (string.IsNullOrWhiteSpace (currentWord)) {
 				ClearSuggestions ();
@@ -524,11 +543,12 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Returns the currently selected word from the <see cref="HostControl"/>.
 		/// <para>
-		/// When overriding this method views can make use of <see cref="IdxToWord(List{Rune}, int)"/>
+		/// When overriding this method views can make use of <see cref="IdxToWord(List{Rune}, int, int)"/>
 		/// </para>
 		/// </summary>
+		/// <param name="columnOffset">The column offset.</param>
 		/// <returns></returns>
-		protected abstract string GetCurrentWord ();
+		protected abstract string GetCurrentWord (int columnOffset = 0);
 
 		/// <summary>
 		/// <para>
@@ -536,37 +556,40 @@ namespace Terminal.Gui {
 		/// or null.  Also returns null if the <paramref name="idx"/> is positioned in the middle of a word.
 		/// </para>
 		/// 
-		/// <para>Use this method to determine whether autocomplete should be shown when the cursor is at
-		/// a given point in a line and to get the word from which suggestions should be generated.</para>
+		/// <para>
+		/// Use this method to determine whether autocomplete should be shown when the cursor is at
+		/// a given point in a line and to get the word from which suggestions should be generated.
+		/// Use the <paramref name="columnOffset"/> to indicate if search the word at left (negative),
+		/// at right (positive) or at the current column (zero) which is the default.
+		/// </para>
 		/// </summary>
 		/// <param name="line"></param>
 		/// <param name="idx"></param>
+		/// <param name="columnOffset"></param>
 		/// <returns></returns>
-		protected virtual string IdxToWord (List<Rune> line, int idx)
+		protected virtual string IdxToWord (List<Rune> line, int idx, int columnOffset = 0)
 		{
 			StringBuilder sb = new StringBuilder ();
+			var endIdx = idx;
 
-			// do not generate suggestions if the cursor is positioned in the middle of a word
-			bool areMidWord;
-
-			if (idx == line.Count) {
-				// the cursor positioned at the very end of the line
-				areMidWord = false;
-			} else {
-				// we are in the middle of a word if the cursor is over a letter/number
-				areMidWord = IsWordChar (line [idx]);
+			// get the ending word index
+			while (endIdx < line.Count) {
+				if (IsWordChar (line [endIdx])) {
+					endIdx++;
+				} else {
+					break;
+				}
 			}
 
-			// if we are in the middle of a word then there is no way to autocomplete that word
-			if (areMidWord) {
+			// It isn't a word char then there is no way to autocomplete that word
+			if (endIdx == idx && columnOffset != 0) {
 				return null;
 			}
 
 			// we are at the end of a word.  Work out what has been typed so far
-			while (idx-- > 0) {
-
-				if (IsWordChar (line [idx])) {
-					sb.Insert (0, (char)line [idx]);
+			while (endIdx-- > 0) {
+				if (IsWordChar (line [endIdx])) {
+					sb.Insert (0, (char)line [endIdx]);
 				} else {
 					break;
 				}

+ 2 - 1
Terminal.Gui/Core/Autocomplete/IAutocomplete.cs

@@ -109,6 +109,7 @@ namespace Terminal.Gui {
 		/// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
 		/// match with the current cursor position/text in the <see cref="HostControl"/>.
 		/// </summary>
-		void GenerateSuggestions ();
+		/// <param name="columnOffset">The column offset. Current (zero - default), left (negative), right (positive).</param>
+		void GenerateSuggestions (int columnOffset = 0);
 	}
 }

+ 304 - 66
Terminal.Gui/Core/ConsoleDriver.cs

@@ -1,23 +1,22 @@
 //
-// ConsoleDriver.cs: Definition for the Console Driver API
+// ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations.
 //
-// Authors:
-//   Miguel de Icaza ([email protected])
-//
-// Define this to enable diagnostics drawing for Window Frames
 using NStack;
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Diagnostics;
 using System.Linq;
 using System.Runtime.CompilerServices;
 using System.Threading.Tasks;
-using Unix.Terminal;
 
 namespace Terminal.Gui {
 	/// <summary>
-	/// Basic colors that can be used to set the foreground and background colors in console applications.
+	/// Colors that can be used to set the foreground and background colors in console applications.
 	/// </summary>
+	/// <remarks>
+	/// The <see cref="Attribute.HasValidColors"/> value indicates either no-color has been set or the color is invalid.
+	/// </remarks>
 	public enum Color {
 		/// <summary>
 		/// The black color.
@@ -86,22 +85,104 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// Attributes are used as elements that contain both a foreground and a background or platform specific features
+	/// 
+	/// </summary>
+	public class TrueColor {
+		/// <summary>
+		/// Red color component.
+		/// </summary>
+		public int Red { get; }
+		/// <summary>
+		/// Green color component.
+		/// </summary>
+		public int Green { get; }
+		/// <summary>
+		/// Blue color component.
+		/// </summary>
+		public int Blue { get; }
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="TrueColor"/> struct.
+		/// </summary>
+		/// <param name="red"></param>
+		/// <param name="green"></param>
+		/// <param name="blue"></param>
+		public TrueColor (int red, int green, int blue)
+		{
+			Red = red;
+			Green = green;
+			Blue = blue;
+		}
+
+		/// <summary>
+		/// 
+		/// </summary>
+		/// <returns></returns>
+		public Color ToConsoleColor ()
+		{
+			var trueColorMap = new Dictionary<TrueColor, Color> () {
+				{ new TrueColor (0,0,0),Color.Black},
+				{ new TrueColor (0, 0, 0x80),Color.Blue},
+				{ new TrueColor (0, 0x80, 0),Color.Green},
+				{ new TrueColor (0, 0x80, 0x80),Color.Cyan},
+				{ new TrueColor (0x80, 0, 0),Color.Red},
+				{ new TrueColor (0x80, 0, 0x80),Color.Magenta},
+				{ new TrueColor (0xC1, 0x9C, 0x00),Color.Brown},  // TODO confirm this
+				{ new TrueColor (0xC0, 0xC0, 0xC0),Color.Gray},
+				{ new TrueColor (0x80, 0x80, 0x80),Color.DarkGray},
+				{ new TrueColor (0, 0, 0xFF),Color.BrightBlue},
+				{ new TrueColor (0, 0xFF, 0),Color.BrightGreen},
+				{ new TrueColor (0, 0xFF, 0xFF),Color.BrightCyan},
+				{ new TrueColor (0xFF, 0, 0),Color.BrightRed},
+				{ new TrueColor (0xFF, 0, 0xFF),Color.BrightMagenta },
+				{ new TrueColor (0xFF, 0xFF, 0),Color.BrightYellow},
+				{ new TrueColor (0xFF, 0xFF, 0xFF),Color.White},
+				};
+			// Iterate over all colors in the map
+			var distances = trueColorMap.Select (
+							k => Tuple.Create (
+								// the candidate we are considering matching against (RGB)
+								k.Key,
+
+								CalculateDistance (k.Key, this)
+							));
+
+			// get the closest
+			var match = distances.OrderBy (t => t.Item2).First ();
+			return trueColorMap [match.Item1];
+		}
+
+		private float CalculateDistance (TrueColor color1, TrueColor color2)
+		{
+			// use RGB distance
+			return
+				Math.Abs (color1.Red - color2.Red) +
+				Math.Abs (color1.Green - color2.Green) +
+				Math.Abs (color1.Blue - color2.Blue);
+		}
+	}
+
+	/// <summary>
+	/// Attributes are used as elements that contain both a foreground and a background or platform specific features.
 	/// </summary>
 	/// <remarks>
-	///   <see cref="Attribute"/>s are needed to map colors to terminal capabilities that might lack colors, on color
-	///   scenarios, they encode both the foreground and the background color and are used in the <see cref="ColorScheme"/>
-	///   class to define color schemes that can be used in your application.
+	///   <see cref="Attribute"/>s are needed to map colors to terminal capabilities that might lack colors. 
+	///   They encode both the foreground and the background color and are used in the <see cref="ColorScheme"/>
+	///   class to define color schemes that can be used in an application.
 	/// </remarks>
 	public struct Attribute {
 		/// <summary>
-		/// The color attribute value.
+		/// The <see cref="ConsoleDriver"/>-specific color attribute value. If <see cref="Initialized"/> is <see langword="false"/> 
+		/// the value of this property is invalid (typically because the Attribute was created before a driver was loaded)
+		/// and the attribute should be re-made (see <see cref="Make(Color, Color)"/>) before it is used.
 		/// </summary>
 		public int Value { get; }
+
 		/// <summary>
 		/// The foreground color.
 		/// </summary>
 		public Color Foreground { get; }
+
 		/// <summary>
 		/// The background color.
 		/// </summary>
@@ -117,8 +198,10 @@ namespace Terminal.Gui {
 			Color foreground = default;
 			Color background = default;
 
+			Initialized = false;
 			if (Application.Driver != null) {
 				Application.Driver.GetColors (value, out foreground, out background);
+				Initialized = true;
 			}
 			Value = value;
 			Foreground = foreground;
@@ -136,6 +219,7 @@ namespace Terminal.Gui {
 			Value = value;
 			Foreground = foreground;
 			Background = background;
+			Initialized = true;
 		}
 
 		/// <summary>
@@ -145,7 +229,9 @@ namespace Terminal.Gui {
 		/// <param name="background">Background</param>
 		public Attribute (Color foreground = new Color (), Color background = new Color ())
 		{
-			Value = Make (foreground, background).Value;
+			var make = Make (foreground, background);
+			Initialized = make.Initialized;
+			Value = make.Value;
 			Foreground = foreground;
 			Background = background;
 		}
@@ -158,29 +244,42 @@ namespace Terminal.Gui {
 		public Attribute (Color color) : this (color, color) { }
 
 		/// <summary>
-		/// Implicit conversion from an <see cref="Attribute"/> to the underlying Int32 representation
+		/// Implicit conversion from an <see cref="Attribute"/> to the underlying, driver-specific, Int32 representation
+		/// of the color.
 		/// </summary>
-		/// <returns>The integer value stored in the attribute.</returns>
+		/// <returns>The driver-specific color value stored in the attribute.</returns>
 		/// <param name="c">The attribute to convert</param>
-		public static implicit operator int (Attribute c) => c.Value;
+		public static implicit operator int (Attribute c)
+		{
+			if (!c.Initialized) throw new InvalidOperationException ("Attribute: Attributes must be initialized by a driver before use.");
+			return c.Value;
+		}
 
 		/// <summary>
-		/// Implicitly convert an integer value into an <see cref="Attribute"/>
+		/// Implicitly convert an driver-specific color value into an <see cref="Attribute"/>
 		/// </summary>
-		/// <returns>An attribute with the specified integer value.</returns>
+		/// <returns>An attribute with the specified driver-specific color value.</returns>
 		/// <param name="v">value</param>
 		public static implicit operator Attribute (int v) => new Attribute (v);
 
 		/// <summary>
-		/// Creates an <see cref="Attribute"/> from the specified foreground and background.
+		/// Creates an <see cref="Attribute"/> from the specified foreground and background colors.
 		/// </summary>
-		/// <returns>The make.</returns>
+		/// <remarks>
+		/// If a <see cref="ConsoleDriver"/> has not been loaded (<c>Application.Driver == null</c>) this
+		/// method will return an attribute with <see cref="Initialized"/> set to  <see langword="false"/>.
+		/// </remarks>
+		/// <returns>The new attribute.</returns>
 		/// <param name="foreground">Foreground color to use.</param>
 		/// <param name="background">Background color to use.</param>
 		public static Attribute Make (Color foreground, Color background)
 		{
-			if (Application.Driver == null)
-				throw new InvalidOperationException ("The Application has not been initialized");
+			if (Application.Driver == null) {
+				// Create the attribute, but show it's not been initialized
+				return new Attribute (-1, foreground, background) {
+					Initialized = false
+				};
+			}
 			return Application.Driver.MakeAttribute (foreground, background);
 		}
 
@@ -194,45 +293,110 @@ namespace Terminal.Gui {
 				throw new InvalidOperationException ("The Application has not been initialized");
 			return Application.Driver.GetAttribute ();
 		}
+
+		/// <summary>
+		/// If <see langword="true"/> the attribute has been initialized by a <see cref="ConsoleDriver"/> and 
+		/// thus has <see cref="Value"/> that is valid for that driver. If <see langword="false"/> the <see cref="Foreground"/>
+		/// and <see cref="Background"/> colors may have been set '-1' but
+		/// the attribute has not been mapped to a <see cref="ConsoleDriver"/> specific color value.
+		/// </summary>
+		/// <remarks>
+		/// Attributes that have not been initialized must eventually be initialized before being passed to a driver.
+		/// </remarks>
+		public bool Initialized { get; internal set; }
+
+		/// <summary>
+		/// Returns <see langword="true"/> if the Attribute is valid (both foreground and background have valid color values).
+		/// </summary>
+		/// <returns></returns>
+		public bool HasValidColors { get => (int)Foreground > -1 && (int)Background > -1; }
 	}
 
 	/// <summary>
-	/// Color scheme definitions, they cover some common scenarios and are used
-	/// typically in containers such as <see cref="Window"/> and <see cref="FrameView"/> to set the scheme that is used by all the
-	/// views contained inside.
+	/// Defines the color <see cref="Attribute"/>s for common visible elements in a <see cref="View"/>. 
+	/// Containers such as <see cref="Window"/> and <see cref="FrameView"/> use <see cref="ColorScheme"/> to determine
+	/// the colors used by sub-views.
 	/// </summary>
+	/// <remarks>
+	/// See also: <see cref="Colors.ColorSchemes"/>.
+	/// </remarks>
 	public class ColorScheme : IEquatable<ColorScheme> {
-		Attribute _normal;
-		Attribute _focus;
-		Attribute _hotNormal;
-		Attribute _hotFocus;
-		Attribute _disabled;
-		internal string caller = "";
+		Attribute _normal = new Attribute (Color.White, Color.Black);
+		Attribute _focus = new Attribute (Color.White, Color.Black);
+		Attribute _hotNormal = new Attribute (Color.White, Color.Black);
+		Attribute _hotFocus = new Attribute (Color.White, Color.Black);
+		Attribute _disabled = new Attribute (Color.White, Color.Black);
+
+		/// <summary>
+		/// Used by <see cref="Colors.SetColorScheme(ColorScheme, string)"/> and <see cref="Colors.GetColorScheme(string)"/> to track which ColorScheme 
+		/// is being accessed.
+		/// </summary>
+		internal string schemeBeingSet = "";
 
 		/// <summary>
-		/// The default color for text, when the view is not focused.
+		/// The foreground and background color for text when the view is not focused, hot, or disabled.
 		/// </summary>
-		public Attribute Normal { get { return _normal; } set { _normal = value; } }
+		public Attribute Normal {
+			get { return _normal; }
+			set {
+				if (!value.HasValidColors) {
+					return;
+				}
+				_normal = value;
+			}
+		}
 
 		/// <summary>
-		/// The color for text when the view has the focus.
+		/// The foreground and background color for text when the view has the focus.
 		/// </summary>
-		public Attribute Focus { get { return _focus; } set { _focus = value; } }
+		public Attribute Focus {
+			get { return _focus; }
+			set {
+				if (!value.HasValidColors) {
+					return;
+				}
+				_focus = value;
+			}
+		}
 
 		/// <summary>
-		/// The color for the hotkey when a view is not focused
+		/// The foreground and background color for text when the view is highlighted (hot).
 		/// </summary>
-		public Attribute HotNormal { get { return _hotNormal; } set { _hotNormal = value; } }
+		public Attribute HotNormal {
+			get { return _hotNormal; }
+			set {
+				if (!value.HasValidColors) {
+					return;
+				}
+				_hotNormal = value;
+			}
+		}
 
 		/// <summary>
-		/// The color for the hotkey when the view is focused.
+		/// The foreground and background color for text when the view is highlighted (hot) and has focus.
 		/// </summary>
-		public Attribute HotFocus { get { return _hotFocus; } set { _hotFocus = value; } }
+		public Attribute HotFocus {
+			get { return _hotFocus; }
+			set {
+				if (!value.HasValidColors) {
+					return;
+				}
+				_hotFocus = value;
+			}
+		}
 
 		/// <summary>
-		/// The default color for text, when the view is disabled.
+		/// The default foreground and background color for text, when the view is disabled.
 		/// </summary>
-		public Attribute Disabled { get { return _disabled; } set { _disabled = value; } }
+		public Attribute Disabled {
+			get { return _disabled; }
+			set {
+				if (!value.HasValidColors) {
+					return;
+				}
+				_disabled = value;
+			}
+		}
 
 		/// <summary>
 		/// Compares two <see cref="ColorScheme"/> objects for equality.
@@ -295,20 +459,67 @@ namespace Terminal.Gui {
 		{
 			return !(left == right);
 		}
+
+		internal void Initialize ()
+		{
+			// If the new scheme was created before a driver was loaded, we need to re-make
+			// the attributes
+			if (!_normal.Initialized) {
+				_normal = new Attribute (_normal.Foreground, _normal.Background);
+			}
+			if (!_focus.Initialized) {
+				_focus = new Attribute (_focus.Foreground, _focus.Background);
+			}
+			if (!_hotNormal.Initialized) {
+				_hotNormal = new Attribute (_hotNormal.Foreground, _hotNormal.Background);
+			}
+			if (!_hotFocus.Initialized) {
+				_hotFocus = new Attribute (_hotFocus.Foreground, _hotFocus.Background);
+			}
+			if (!_disabled.Initialized) {
+				_disabled = new Attribute (_disabled.Foreground, _disabled.Background);
+			}
+		}
 	}
 
 	/// <summary>
 	/// The default <see cref="ColorScheme"/>s for the application.
 	/// </summary>
+	/// <remarks>
+	/// This property can be set in a Theme to change the default <see cref="Colors"/> for the application.
+	/// </remarks>
 	public static class Colors {
+		private class SchemeNameComparerIgnoreCase : IEqualityComparer<string> {
+			public bool Equals (string x, string y)
+			{
+				if (x != null && y != null) {
+					return x.ToLowerInvariant () == y.ToLowerInvariant ();
+				}
+				return false;
+			}
+
+			public int GetHashCode (string obj)
+			{
+				return obj.ToLowerInvariant ().GetHashCode ();
+			}
+		}
+
 		static Colors ()
+		{
+			ColorSchemes = Create ();
+		}
+
+		/// <summary>
+		/// Creates a new dictionary of new <see cref="ColorScheme"/> objects.
+		/// </summary>
+		public static Dictionary<string, ColorScheme> Create ()
 		{
 			// Use reflection to dynamically create the default set of ColorSchemes from the list defined 
 			// by the class. 
-			ColorSchemes = typeof (Colors).GetProperties ()
+			return typeof (Colors).GetProperties ()
 				.Where (p => p.PropertyType == typeof (ColorScheme))
-				.Select (p => new KeyValuePair<string, ColorScheme> (p.Name, new ColorScheme ())) // (ColorScheme)p.GetValue (p)))
-				.ToDictionary (t => t.Key, t => t.Value);
+				.Select (p => new KeyValuePair<string, ColorScheme> (p.Name, new ColorScheme ()))
+				.ToDictionary (t => t.Key, t => t.Value, comparer: new SchemeNameComparerIgnoreCase ());
 		}
 
 		/// <summary>
@@ -361,21 +572,21 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public static ColorScheme Error { get => GetColorScheme (); set => SetColorScheme (value); }
 
-		static ColorScheme GetColorScheme ([CallerMemberName] string callerMemberName = null)
+		static ColorScheme GetColorScheme ([CallerMemberName] string schemeBeingSet = null)
 		{
-			return ColorSchemes [callerMemberName];
+			return ColorSchemes [schemeBeingSet];
 		}
 
-		static void SetColorScheme (ColorScheme colorScheme, [CallerMemberName] string callerMemberName = null)
+		static void SetColorScheme (ColorScheme colorScheme, [CallerMemberName] string schemeBeingSet = null)
 		{
-			ColorSchemes [callerMemberName] = colorScheme;
-			colorScheme.caller = callerMemberName;
+			ColorSchemes [schemeBeingSet] = colorScheme;
+			colorScheme.schemeBeingSet = schemeBeingSet;
 		}
 
 		/// <summary>
 		/// Provides the defined <see cref="ColorScheme"/>s.
 		/// </summary>
-		public static Dictionary<string, ColorScheme> ColorSchemes { get; }
+		public static Dictionary<string, ColorScheme> ColorSchemes { get; private set; }
 	}
 
 	/// <summary>
@@ -659,13 +870,35 @@ namespace Terminal.Gui {
 		public abstract void UpdateScreen ();
 
 		/// <summary>
-		/// Selects the specified attribute as the attribute to use for future calls to AddRune, AddString.
+		/// The current attribute the driver is using. 
 		/// </summary>
+		public virtual Attribute CurrentAttribute {
+			get => currentAttribute;
+			set {
+				if (!value.Initialized && value.HasValidColors && Application.Driver != null) {
+					CurrentAttribute = Application.Driver.MakeAttribute (value.Foreground, value.Background);
+					return;
+				}
+				if (!value.Initialized) Debug.WriteLine ("ConsoleDriver.CurrentAttribute: Attributes must be initialized before use.");
+
+				currentAttribute = value;
+			}
+		}
+
+		/// <summary>
+		/// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.
+		/// </summary>
+		/// <remarks>
+		/// Implementations should call <c>base.SetAttribute(c)</c>.
+		/// </remarks>
 		/// <param name="c">C.</param>
-		public abstract void SetAttribute (Attribute c);
+		public virtual void SetAttribute (Attribute c)
+		{
+			CurrentAttribute = c;
+		}
 
 		/// <summary>
-		/// Set Colors from limit sets of colors.
+		/// Set Colors from limit sets of colors. Not implemented by any driver: See Issue #2300.
 		/// </summary>
 		/// <param name="foreground">Foreground.</param>
 		/// <param name="background">Background.</param>
@@ -675,7 +908,7 @@ namespace Terminal.Gui {
 		// that independently with the R, G, B values.
 		/// <summary>
 		/// Advanced uses - set colors to any pre-set pairs, you would need to init_color
-		/// that independently with the R, G, B values.
+		/// that independently with the R, G, B values. Not implemented by any driver: See Issue #2300.
 		/// </summary>
 		/// <param name="foregroundColorId">Foreground color identifier.</param>
 		/// <param name="backgroundColorId">Background color identifier.</param>
@@ -998,12 +1231,13 @@ namespace Terminal.Gui {
 		public abstract void StopReportingMouseMoves ();
 
 		/// <summary>
-		/// Disables the cooked event processing from the mouse driver.  At startup, it is assumed mouse events are cooked.
+		/// Disables the cooked event processing from the mouse driver. 
+		/// At startup, it is assumed mouse events are cooked. Not implemented by any driver: See Issue #2300.
 		/// </summary>
 		public abstract void UncookMouse ();
 
 		/// <summary>
-		/// Enables the cooked event processing from the mouse driver
+		/// Enables the cooked event processing from the mouse driver. Not implemented by any driver: See Issue #2300.
 		/// </summary>
 		public abstract void CookMouse ();
 
@@ -1201,6 +1435,7 @@ namespace Terminal.Gui {
 		/// Lower right rounded corner
 		/// </summary>
 		public Rune LRRCorner = '\u256f';
+		private Attribute currentAttribute;
 
 		/// <summary>
 		/// Make the attribute for the foreground and background colors.
@@ -1214,7 +1449,7 @@ namespace Terminal.Gui {
 		/// Gets the current <see cref="Attribute"/>.
 		/// </summary>
 		/// <returns>The current attribute.</returns>
-		public abstract Attribute GetAttribute ();
+		public Attribute GetAttribute () => CurrentAttribute;
 
 		/// <summary>
 		/// Make the <see cref="Colors"/> for the <see cref="ColorScheme"/>.
@@ -1225,21 +1460,24 @@ namespace Terminal.Gui {
 		public abstract Attribute MakeColor (Color foreground, Color background);
 
 		/// <summary>
-		/// Create all <see cref="Colors"/> with the <see cref="ColorScheme"/> for the console driver.
+		/// Ensures all <see cref="Attribute"/>s in <see cref="Colors.ColorSchemes"/> are correctly 
+		/// initialized by the driver.
 		/// </summary>
-		/// <param name="hasColors">Flag indicating if colors are supported.</param>
-		public void CreateColors (bool hasColors = true)
+		/// <param name="supportsColors">Flag indicating if colors are supported (not used).</param>
+		public void InitalizeColorSchemes (bool supportsColors = true)
 		{
-			Colors.TopLevel = new ColorScheme ();
-			Colors.Base = new ColorScheme ();
-			Colors.Dialog = new ColorScheme ();
-			Colors.Menu = new ColorScheme ();
-			Colors.Error = new ColorScheme ();
+			// Ensure all Attributes are initialized by the driver
+			foreach (var s in Colors.ColorSchemes) {
+				s.Value.Initialize ();
+			}
 
-			if (!hasColors) {
+			if (!supportsColors) {
 				return;
 			}
 
+
+			// Define the default color theme only if the user has not defined one.
+
 			Colors.TopLevel.Normal = MakeColor (Color.BrightGreen, Color.Black);
 			Colors.TopLevel.Focus = MakeColor (Color.White, Color.Cyan);
 			Colors.TopLevel.HotNormal = MakeColor (Color.Brown, Color.Black);

+ 605 - 0
Terminal.Gui/Core/Graphs/LineCanvas.cs

@@ -0,0 +1,605 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Terminal.Gui.Graphs {
+
+
+	/// <summary>
+	/// Facilitates box drawing and line intersection detection
+	/// and rendering.  Does not support diagonal lines.
+	/// </summary>
+	public class LineCanvas {
+
+
+		private List<StraightLine> lines = new List<StraightLine> ();
+
+		Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> {
+			{IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()},
+			{IntersectionRuneType.URCorner,new URIntersectionRuneResolver()},
+			{IntersectionRuneType.LLCorner,new LLIntersectionRuneResolver()},
+			{IntersectionRuneType.LRCorner,new LRIntersectionRuneResolver()},
+
+			{IntersectionRuneType.TopTee,new TopTeeIntersectionRuneResolver()},
+			{IntersectionRuneType.LeftTee,new LeftTeeIntersectionRuneResolver()},
+			{IntersectionRuneType.RightTee,new RightTeeIntersectionRuneResolver()},
+			{IntersectionRuneType.BottomTee,new BottomTeeIntersectionRuneResolver()},
+
+
+			{IntersectionRuneType.Crosshair,new CrosshairIntersectionRuneResolver()},
+			// TODO: Add other resolvers
+		};
+
+		/// <summary>
+		/// Add a new line to the canvas starting at <paramref name="from"/>.
+		/// Use positive <paramref name="length"/> for Right and negative for Left
+		/// when <see cref="Orientation"/> is <see cref="Orientation.Horizontal"/>.
+		/// Use positive <paramref name="length"/> for Down and negative for Up
+		/// when <see cref="Orientation"/> is <see cref="Orientation.Vertical"/>.
+		/// </summary>
+		/// <param name="from">Starting point.</param>
+		/// <param name="length">Length of line.  0 for a dot.  
+		/// Positive for Down/Right.  Negative for Up/Left.</param>
+		/// <param name="orientation">Direction of the line.</param>
+		/// <param name="style">The style of line to use</param>
+		public void AddLine (Point from, int length, Orientation orientation, BorderStyle style)
+		{
+			lines.Add (new StraightLine (from, length, orientation, style));
+		}
+		/// <summary>
+		/// Evaluate all currently defined lines that lie within 
+		/// <paramref name="inArea"/> and map that
+		/// shows what characters (if any) should be rendered at each
+		/// point so that all lines connect up correctly with appropriate
+		/// intersection symbols.
+		/// <returns></returns>
+		/// </summary>
+		/// <param name="inArea"></param>
+		/// <returns>Mapping of all the points within <paramref name="inArea"/> to
+		/// line or intersection runes which should be drawn there.</returns>
+		public Dictionary<Point,Rune> GenerateImage (Rect inArea)
+		{
+			var map = new Dictionary<Point,Rune>();
+
+			// walk through each pixel of the bitmap
+			for (int y = inArea.Y; y < inArea.Height; y++) {
+				for (int x = inArea.X; x < inArea.Width; x++) {
+
+					var intersects = lines
+						.Select (l => l.Intersects (x, y))
+						.Where (i => i != null)
+						.ToArray ();
+
+					// TODO: use Driver and LineStyle to map
+					var rune = GetRuneForIntersects (Application.Driver, intersects);
+
+					if(rune != null)
+					{
+						map.Add(new Point(x,y),rune.Value);
+					}
+				}
+			}
+
+			return map;
+		}
+
+		private abstract class IntersectionRuneResolver {
+			readonly Rune round;
+			readonly Rune doubleH;
+			readonly Rune doubleV;
+			readonly Rune doubleBoth;
+			readonly Rune normal;
+
+			public IntersectionRuneResolver (Rune round, Rune doubleH, Rune doubleV, Rune doubleBoth, Rune normal)
+			{
+				this.round = round;
+				this.doubleH = doubleH;
+				this.doubleV = doubleV;
+				this.doubleBoth = doubleBoth;
+				this.normal = normal;
+			}
+
+			public Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
+			{
+				var useRounded = intersects.Any (i => i.Line.Style == BorderStyle.Rounded && i.Line.Length != 0);
+
+				bool doubleHorizontal = intersects.Any (l => l.Line.Orientation == Orientation.Horizontal && l.Line.Style == BorderStyle.Double);
+				bool doubleVertical = intersects.Any (l => l.Line.Orientation == Orientation.Vertical && l.Line.Style == BorderStyle.Double);
+
+
+				if (doubleHorizontal) {
+					return doubleVertical ? doubleBoth : doubleH;
+				}
+
+				if (doubleVertical) {
+					return doubleV;
+				}
+
+				return useRounded ? round : normal;
+			}
+		}
+
+		private class ULIntersectionRuneResolver : IntersectionRuneResolver {
+			public ULIntersectionRuneResolver () :
+				base ('╭', '╒', '╓', '╔', '┌')
+			{
+
+			}
+		}
+		private class URIntersectionRuneResolver : IntersectionRuneResolver {
+
+			public URIntersectionRuneResolver () :
+				base ('╮', '╕', '╖', '╗', '┐')
+			{
+
+			}
+		}
+		private class LLIntersectionRuneResolver : IntersectionRuneResolver {
+
+			public LLIntersectionRuneResolver () :
+				base ('╰', '╘', '╙', '╚', '└')
+			{
+
+			}
+		}
+		private class LRIntersectionRuneResolver : IntersectionRuneResolver {
+			public LRIntersectionRuneResolver () :
+				base ('╯', '╛', '╜', '╝', '┘')
+			{
+
+			}
+		}
+
+		private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver {
+			public TopTeeIntersectionRuneResolver () :
+				base ('┬', '╤', '╥', '╦', '┬')
+			{
+
+			}
+		}
+		private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver {
+			public LeftTeeIntersectionRuneResolver () :
+				base ('├', '╞', '╟', '╠', '├')
+			{
+
+			}
+		}
+		private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver {
+			public RightTeeIntersectionRuneResolver () :
+				base ('┤', '╡', '╢', '╣', '┤')
+			{
+
+			}
+		}
+		private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver {
+			public BottomTeeIntersectionRuneResolver () :
+				base ('┴', '╧', '╨', '╩', '┴')
+			{
+
+			}
+		}
+		private class CrosshairIntersectionRuneResolver : IntersectionRuneResolver {
+			public CrosshairIntersectionRuneResolver () :
+				base ('┼', '╪', '╫', '╬', '┼')
+			{
+
+			}
+		}
+
+		private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
+		{
+			if (!intersects.Any ())
+				return null;
+
+			var runeType = GetRuneTypeForIntersects (intersects);
+
+			if (runeResolvers.ContainsKey (runeType)) {
+				return runeResolvers [runeType].GetRuneForIntersects (driver, intersects);
+			}
+
+			// TODO: Remove these two once we have all of the below ported to IntersectionRuneResolvers
+			var useDouble = intersects.Any (i => i.Line.Style == BorderStyle.Double && i.Line.Length != 0);
+			var useRounded = intersects.Any (i => i.Line.Style == BorderStyle.Rounded && i.Line.Length != 0);
+
+			// TODO: maybe make these resolvers to for simplicity?
+			// or for dotted lines later on or that kind of thing?
+			switch (runeType) {
+			case IntersectionRuneType.None:
+				return null;
+			case IntersectionRuneType.Dot:
+				return (Rune)'.';
+			case IntersectionRuneType.HLine:
+				return useDouble ? driver.HDLine : driver.HLine;
+			case IntersectionRuneType.VLine:
+				return useDouble ? driver.VDLine : driver.VLine;
+			default: throw new Exception ("Could not find resolver or switch case for " + nameof (runeType) + ":" + runeType);
+			}
+		}
+
+
+		private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition [] intersects)
+		{
+			if (intersects.All (i => i.Line.Length == 0)) {
+				return IntersectionRuneType.Dot;
+			}
+
+			// ignore dots
+			intersects = intersects.Where (i => i.Type != IntersectionType.Dot).ToArray ();
+
+			var set = new HashSet<IntersectionType> (intersects.Select (i => i.Type));
+
+			#region Crosshair Conditions
+			if (Has (set,
+				IntersectionType.PassOverHorizontal,
+				IntersectionType.PassOverVertical
+				)) {
+				return IntersectionRuneType.Crosshair;
+			}
+
+			if (Has (set,
+				IntersectionType.PassOverVertical,
+				IntersectionType.StartLeft,
+				IntersectionType.StartRight
+				)) {
+				return IntersectionRuneType.Crosshair;
+			}
+
+			if (Has (set,
+				IntersectionType.PassOverHorizontal,
+				IntersectionType.StartUp,
+				IntersectionType.StartDown
+				)) {
+				return IntersectionRuneType.Crosshair;
+			}
+
+
+			if (Has (set,
+				IntersectionType.StartLeft,
+				IntersectionType.StartRight,
+				IntersectionType.StartUp,
+				IntersectionType.StartDown)) {
+				return IntersectionRuneType.Crosshair;
+			}
+			#endregion
+
+
+			#region Corner Conditions
+			if (Exactly (set,
+				IntersectionType.StartRight,
+				IntersectionType.StartDown)) {
+				return IntersectionRuneType.ULCorner;
+			}
+
+			if (Exactly (set,
+				IntersectionType.StartLeft,
+				IntersectionType.StartDown)) {
+				return IntersectionRuneType.URCorner;
+			}
+
+			if (Exactly (set,
+				IntersectionType.StartUp,
+				IntersectionType.StartLeft)) {
+				return IntersectionRuneType.LRCorner;
+			}
+
+			if (Exactly (set,
+				IntersectionType.StartUp,
+				IntersectionType.StartRight)) {
+				return IntersectionRuneType.LLCorner;
+			}
+			#endregion Corner Conditions
+
+			#region T Conditions
+			if (Has (set,
+				IntersectionType.PassOverHorizontal,
+				IntersectionType.StartDown)) {
+				return IntersectionRuneType.TopTee;
+			}
+			if (Has (set,
+				IntersectionType.StartRight,
+				IntersectionType.StartLeft,
+				IntersectionType.StartDown)) {
+				return IntersectionRuneType.TopTee;
+			}
+
+			if (Has (set,
+				IntersectionType.PassOverHorizontal,
+				IntersectionType.StartUp)) {
+				return IntersectionRuneType.BottomTee;
+			}
+			if (Has (set,
+				IntersectionType.StartRight,
+				IntersectionType.StartLeft,
+				IntersectionType.StartUp)) {
+				return IntersectionRuneType.BottomTee;
+			}
+
+
+			if (Has (set,
+				IntersectionType.PassOverVertical,
+				IntersectionType.StartRight)) {
+				return IntersectionRuneType.LeftTee;
+			}
+			if (Has (set,
+				IntersectionType.StartRight,
+				IntersectionType.StartDown,
+				IntersectionType.StartUp)) {
+				return IntersectionRuneType.LeftTee;
+			}
+
+
+			if (Has (set,
+				IntersectionType.PassOverVertical,
+				IntersectionType.StartLeft)) {
+				return IntersectionRuneType.RightTee;
+			}
+			if (Has (set,
+				IntersectionType.StartLeft,
+				IntersectionType.StartDown,
+				IntersectionType.StartUp)) {
+				return IntersectionRuneType.RightTee;
+			}
+			#endregion
+
+			if (All (intersects, Orientation.Horizontal)) {
+				return IntersectionRuneType.HLine;
+			}
+
+			if (All (intersects, Orientation.Vertical)) {
+				return IntersectionRuneType.VLine;
+			}
+
+			return IntersectionRuneType.Dot;
+		}
+
+		private bool All (IntersectionDefinition [] intersects, Orientation orientation)
+		{
+			return intersects.All (i => i.Line.Orientation == orientation);
+		}
+
+		/// <summary>
+		/// Returns true if the <paramref name="intersects"/> collection has all the <paramref name="types"/>
+		/// specified (i.e. AND).
+		/// </summary>
+		/// <param name="intersects"></param>
+		/// <param name="types"></param>
+		/// <returns></returns>
+		private bool Has (HashSet<IntersectionType> intersects, params IntersectionType [] types)
+		{
+			return types.All (t => intersects.Contains (t));
+		}
+
+		/// <summary>
+		/// Returns true if all requested <paramref name="types"/> appear in <paramref name="intersects"/>
+		/// and there are no additional <see cref="IntersectionRuneType"/>
+		/// </summary>
+		/// <param name="intersects"></param>
+		/// <param name="types"></param>
+		/// <returns></returns>
+		private bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types)
+		{
+			return intersects.SetEquals (types);
+		}
+
+		class IntersectionDefinition {
+			/// <summary>
+			/// The point at which the intersection happens
+			/// </summary>
+			public Point Point { get; }
+
+			/// <summary>
+			/// Defines how <see cref="Line"/> position relates
+			/// to <see cref="Point"/>.
+			/// </summary>
+			public IntersectionType Type { get; }
+
+			/// <summary>
+			/// The line that intersects <see cref="Point"/>
+			/// </summary>
+			public StraightLine Line { get; }
+
+			public IntersectionDefinition (Point point, IntersectionType type, StraightLine line)
+			{
+				Point = point;
+				Type = type;
+				Line = line;
+			}
+		}
+
+		/// <summary>
+		/// The type of Rune that we will use before considering
+		/// double width, curved borders etc
+		/// </summary>
+		enum IntersectionRuneType {
+			None,
+			Dot,
+			ULCorner,
+			URCorner,
+			LLCorner,
+			LRCorner,
+			TopTee,
+			BottomTee,
+			RightTee,
+			LeftTee,
+			Crosshair,
+			HLine,
+			VLine,
+		}
+
+		enum IntersectionType {
+			/// <summary>
+			/// There is no intersection
+			/// </summary>
+			None,
+
+			/// <summary>
+			///  A line passes directly over this point traveling along
+			///  the horizontal axis
+			/// </summary>
+			PassOverHorizontal,
+
+			/// <summary>
+			///  A line passes directly over this point traveling along
+			///  the vertical axis
+			/// </summary>
+			PassOverVertical,
+
+			/// <summary>
+			/// A line starts at this point and is traveling up
+			/// </summary>
+			StartUp,
+
+			/// <summary>
+			/// A line starts at this point and is traveling right
+			/// </summary>
+			StartRight,
+
+			/// <summary>
+			/// A line starts at this point and is traveling down
+			/// </summary>
+			StartDown,
+
+			/// <summary>
+			/// A line starts at this point and is traveling left
+			/// </summary>
+			StartLeft,
+
+			/// <summary>
+			/// A line exists at this point who has 0 length
+			/// </summary>
+			Dot
+		}
+
+		class StraightLine {
+			public Point Start { get; }
+			public int Length { get; }
+			public Orientation Orientation { get; }
+			public BorderStyle Style { get; }
+
+			public StraightLine (Point start, int length, Orientation orientation, BorderStyle style)
+			{
+				this.Start = start;
+				this.Length = length;
+				this.Orientation = orientation;
+				this.Style = style;
+			}
+
+			internal IntersectionDefinition Intersects (int x, int y)
+			{
+				if (IsDot ()) {
+					if (StartsAt (x, y)) {
+						return new IntersectionDefinition (Start, IntersectionType.Dot, this);
+					} else {
+						return null;
+					}
+				}
+
+				switch (Orientation) {
+				case Orientation.Horizontal: return IntersectsHorizontally (x, y);
+				case Orientation.Vertical: return IntersectsVertically (x, y);
+				default: throw new ArgumentOutOfRangeException (nameof (Orientation));
+				}
+
+			}
+
+			private IntersectionDefinition IntersectsHorizontally (int x, int y)
+			{
+				if (Start.Y != y) {
+					return null;
+				} else {
+					if (StartsAt (x, y)) {
+
+						return new IntersectionDefinition (
+							Start,
+							Length < 0 ? IntersectionType.StartLeft : IntersectionType.StartRight,
+							this
+							);
+
+					}
+
+					if (EndsAt (x, y)) {
+
+						return new IntersectionDefinition (
+							Start,
+							Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft,
+							this
+							);
+
+					} else {
+						var xmin = Math.Min (Start.X, Start.X + Length);
+						var xmax = Math.Max (Start.X, Start.X + Length);
+
+						if (xmin < x && xmax > x) {
+							return new IntersectionDefinition (
+							new Point (x, y),
+							IntersectionType.PassOverHorizontal,
+							this
+							);
+						}
+					}
+
+					return null;
+				}
+			}
+
+			private IntersectionDefinition IntersectsVertically (int x, int y)
+			{
+				if (Start.X != x) {
+					return null;
+				} else {
+					if (StartsAt (x, y)) {
+
+						return new IntersectionDefinition (
+							Start,
+							Length < 0 ? IntersectionType.StartUp : IntersectionType.StartDown,
+							this
+							);
+
+					}
+
+					if (EndsAt (x, y)) {
+
+						return new IntersectionDefinition (
+							Start,
+							Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp,
+							this
+							);
+
+					} else {
+						var ymin = Math.Min (Start.Y, Start.Y + Length);
+						var ymax = Math.Max (Start.Y, Start.Y + Length);
+
+						if (ymin < y && ymax > y) {
+							return new IntersectionDefinition (
+							new Point (x, y),
+							IntersectionType.PassOverVertical,
+							this
+							);
+						}
+					}
+
+					return null;
+				}
+			}
+
+			private bool EndsAt (int x, int y)
+			{
+				if (Orientation == Orientation.Horizontal) {
+					return Start.X + Length == x && Start.Y == y;
+				}
+
+				return Start.X == x && Start.Y + Length == y;
+			}
+
+			private bool StartsAt (int x, int y)
+			{
+				return Start.X == x && Start.Y == y;
+			}
+
+			private bool IsDot ()
+			{
+				return Length == 0;
+			}
+		}
+	}
+}

+ 12 - 1
Terminal.Gui/Core/Toplevel.cs

@@ -821,7 +821,6 @@ namespace Terminal.Gui {
 
 			if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Released) && dragPosition.HasValue) {
 				Application.UngrabMouse ();
-				Driver.UncookMouse ();
 				dragPosition = null;
 			}
 
@@ -960,6 +959,18 @@ namespace Terminal.Gui {
 			}
 			return false;
 		}
+
+		///<inheritdoc/>
+		public override bool OnEnter (View view)
+		{
+			return MostFocused?.OnEnter (view) ?? base.OnEnter (view);
+		}
+
+		///<inheritdoc/>
+		public override bool OnLeave (View view)
+		{
+			return MostFocused?.OnLeave (view) ?? base.OnLeave (view);
+		}
 	}
 
 	/// <summary>

+ 43 - 16
Terminal.Gui/Core/View.cs

@@ -446,14 +446,11 @@ namespace Terminal.Gui {
 		public virtual Rect Frame {
 			get => frame;
 			set {
-				if (SuperView != null) {
-					SuperView.SetNeedsDisplay (frame);
-					SuperView.SetNeedsDisplay (value);
-				}
+				var rect = GetMaxNeedDisplay (frame, value);
 				frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0));
 				TextFormatter.Size = GetBoundsTextFormatterSize ();
 				SetNeedsLayout ();
-				SetNeedsDisplay (frame);
+				SetNeedsDisplay (rect);
 			}
 		}
 
@@ -811,6 +808,7 @@ namespace Terminal.Gui {
 		{
 			var actX = x is Pos.PosAbsolute ? x.Anchor (0) : frame.X;
 			var actY = y is Pos.PosAbsolute ? y.Anchor (0) : frame.Y;
+			Rect oldFrame = frame;
 
 			if (AutoSize) {
 				var s = GetAutoSize ();
@@ -825,7 +823,21 @@ namespace Terminal.Gui {
 			}
 			TextFormatter.Size = GetBoundsTextFormatterSize ();
 			SetNeedsLayout ();
-			SetNeedsDisplay ();
+			SetNeedsDisplay (GetMaxNeedDisplay (oldFrame, frame));
+		}
+
+		Rect GetMaxNeedDisplay (Rect oldFrame, Rect newFrame)
+		{
+			var rect = new Rect () {
+				X = Math.Min (oldFrame.X, newFrame.X),
+				Y = Math.Min (oldFrame.Y, newFrame.Y),
+				Width = Math.Max (oldFrame.Width, newFrame.Width),
+				Height = Math.Max (oldFrame.Height, newFrame.Height)
+			};
+			rect.Width += Math.Max (oldFrame.X - newFrame.X, 0);
+			rect.Height += Math.Max (oldFrame.Y - newFrame.Y, 0);
+
+			return rect;
 		}
 
 		void TextFormatter_HotKeyChanged (Key obj)
@@ -1435,8 +1447,9 @@ namespace Terminal.Gui {
 		/// </summary>
 		public virtual ColorScheme ColorScheme {
 			get {
-				if (colorScheme == null)
+				if (colorScheme == null) {
 					return SuperView?.ColorScheme;
+				}
 				return colorScheme;
 			}
 			set {
@@ -1501,7 +1514,7 @@ namespace Terminal.Gui {
 				Driver.SetAttribute (HasFocus ? ColorScheme.Focus : ColorScheme.Normal);
 			}
 
-			if (Border != null) {
+			if (!IgnoreBorderPropertyOnRedraw && Border != null) {
 				Border.DrawContent (this);
 			} else if (ustring.IsNullOrEmpty (TextFormatter.Text) &&
 				(GetType ().IsNestedPublic) && !IsOverridden (this, "Redraw") &&
@@ -1537,12 +1550,7 @@ namespace Terminal.Gui {
 							// 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) {
-								var rect = new Rect () {
-									X = Math.Min (view.Bounds.X, view.NeedDisplay.X),
-									Y = Math.Min (view.Bounds.Y, view.NeedDisplay.Y),
-									Width = Math.Max (view.Bounds.Width, view.NeedDisplay.Width),
-									Height = Math.Max (view.Bounds.Height, view.NeedDisplay.Height)
-								};
+								var rect = view.Bounds;
 								view.OnDrawContent (rect);
 								view.Redraw (rect);
 								view.OnDrawContentComplete (rect);
@@ -1567,8 +1575,18 @@ namespace Terminal.Gui {
 			var driverClip = Driver == null ? Rect.Empty : Driver.Clip;
 			containerBounds.X = Math.Max (containerBounds.X, driverClip.X);
 			containerBounds.Y = Math.Max (containerBounds.Y, driverClip.Y);
-			containerBounds.Width = Math.Min (containerBounds.Width, driverClip.Width);
-			containerBounds.Height = Math.Min (containerBounds.Height, driverClip.Height);
+			var lenOffset = (driverClip.X + driverClip.Width) - (containerBounds.X + containerBounds.Width);
+			if (containerBounds.X + containerBounds.Width > driverClip.X + driverClip.Width) {
+				containerBounds.Width = Math.Max (containerBounds.Width + lenOffset, 0);
+			} else {
+				containerBounds.Width = Math.Min (containerBounds.Width, driverClip.Width);
+			}
+			lenOffset = (driverClip.Y + driverClip.Height) - (containerBounds.Y + containerBounds.Height);
+			if (containerBounds.Y + containerBounds.Height > driverClip.Y + driverClip.Height) {
+				containerBounds.Height = Math.Max (containerBounds.Height + lenOffset, 0);
+			} else {
+				containerBounds.Height = Math.Min (containerBounds.Height, driverClip.Height);
+			}
 			return containerBounds;
 		}
 
@@ -2655,6 +2673,15 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Get or sets whether the view will use <see cref="Terminal.Gui.Border"/> (if <see cref="Border"/> is set) to draw 
+		/// a border. If <see langword="false"/> (the default),
+		/// <see cref="View.Redraw(Rect)"/> will call <see cref="Border.DrawContent(View, bool)"/>
+		/// to draw the view's border. If <see langword="true"/> no border is drawn (and the view is expected to draw the border
+		/// itself).
+		/// </summary>
+		public virtual bool IgnoreBorderPropertyOnRedraw { get; set; } = false;
+
 		/// <summary>
 		/// Pretty prints the View
 		/// </summary>

+ 5 - 5
Terminal.Gui/Terminal.Gui.csproj

@@ -8,12 +8,12 @@
   </PropertyGroup>
   <PropertyGroup>
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
-    <!-- In the source tree the version will always be 2.0 for all projects. -->
+    <!-- In the source tree the version will always be 1.0 for all projects. -->
     <!-- Do not modify these. Do NOT commit after manually running `dotnet-gitversion /updateprojectfiles` -->
-    <AssemblyVersion>2.0</AssemblyVersion>
-    <FileVersion>2.0</FileVersion>
-    <Version>2.0</Version>
-    <InformationalVersion>2.0</InformationalVersion>
+    <AssemblyVersion>1.0</AssemblyVersion>
+    <FileVersion>1.0</FileVersion>
+    <Version>1.0</Version>
+    <InformationalVersion>1.0</InformationalVersion>
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />

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

@@ -1064,8 +1064,8 @@ namespace Terminal.Gui {
 				EnsureHasFocus ();
 				int x = PositionCursor (ev);
 				int sbw = x;
-				if (x == text.Count || (x > 0 && (char)Text [x - 1] != ' '
-					|| (x > 0 && (char)Text [x] == ' '))) {
+				if (x == text.Count || (x > 0 && (char)text [x - 1] != ' ')
+					|| (x > 0 && (char)text [x] == ' ')) {
 
 					sbw = WordBackward (x);
 				}
@@ -1346,12 +1346,12 @@ namespace Terminal.Gui {
 		}
 
 		/// <inheritdoc/>
-		protected override string GetCurrentWord ()
+		protected override string GetCurrentWord (int columnOffset = 0)
 		{
 			var host = (TextField)HostControl;
 			var currentLine = host.Text.ToRuneList ();
-			var cursorPosition = Math.Min (host.CursorPosition, currentLine.Count);
-			return IdxToWord (currentLine, cursorPosition);
+			var cursorPosition = Math.Min (host.CursorPosition + columnOffset, currentLine.Count);
+			return IdxToWord (currentLine, cursorPosition, columnOffset);
 		}
 
 		/// <inheritdoc/>

+ 23 - 15
Terminal.Gui/Views/TextView.cs

@@ -2035,6 +2035,16 @@ namespace Terminal.Gui {
 			return base.OnEnter (view);
 		}
 
+		///<inheritdoc/>
+		public override bool OnLeave (View view)
+		{
+			if (Application.MouseGrabView != null && Application.MouseGrabView == this) {
+				Application.UngrabMouse ();
+			}
+
+			return base.OnLeave (view);
+		}
+
 		// Returns an encoded region start..end (top 32 bits are the row, low32 the column)
 		void GetEncodedRegionBounds (out long start, out long end,
 			int? startRow = null, int? startCol = null, int? cRow = null, int? cCol = null)
@@ -2437,6 +2447,10 @@ namespace Terminal.Gui {
 
 			PositionCursor ();
 
+			if (clickWithSelecting) {
+				clickWithSelecting = false;
+				return;
+			}
 			if (SelectedLength > 0)
 				return;
 
@@ -2667,8 +2681,10 @@ namespace Terminal.Gui {
 				need = true;
 			} else if ((wordWrap && leftColumn > 0) || (dSize.size + RightOffset < Frame.Width + offB.width
 				&& tSize.size + RightOffset < Frame.Width + offB.width)) {
-				leftColumn = 0;
-				need = true;
+				if (leftColumn > 0) {
+					leftColumn = 0;
+					need = true;
+				}
 			}
 
 			if (currentRow < topRow) {
@@ -4269,6 +4285,7 @@ namespace Terminal.Gui {
 		}
 
 		bool isButtonShift;
+		bool clickWithSelecting;
 
 		///<inheritdoc/>
 		public override bool MouseEvent (MouseEvent ev)
@@ -4362,6 +4379,7 @@ namespace Terminal.Gui {
 				columnTrack = currentColumn;
 			} else if (ev.Flags.HasFlag (MouseFlags.Button1Pressed)) {
 				if (shiftSelecting) {
+					clickWithSelecting = true;
 					StopSelecting ();
 				}
 				ProcessMouseClick (ev, out _);
@@ -4447,16 +4465,6 @@ namespace Terminal.Gui {
 			line = r;
 		}
 
-		///<inheritdoc/>
-		public override bool OnLeave (View view)
-		{
-			if (Application.MouseGrabView != null && Application.MouseGrabView == this) {
-				Application.UngrabMouse ();
-			}
-
-			return base.OnLeave (view);
-		}
-
 		/// <summary>
 		/// Allows clearing the <see cref="HistoryText.HistoryTextItem"/> items updating the original text.
 		/// </summary>
@@ -4475,12 +4483,12 @@ namespace Terminal.Gui {
 	public class TextViewAutocomplete : Autocomplete {
 
 		///<inheritdoc/>
-		protected override string GetCurrentWord ()
+		protected override string GetCurrentWord (int columnOffset = 0)
 		{
 			var host = (TextView)HostControl;
 			var currentLine = host.GetCurrentLine ();
-			var cursorPosition = Math.Min (host.CurrentColumn, currentLine.Count);
-			return IdxToWord (currentLine, cursorPosition);
+			var cursorPosition = Math.Min (host.CurrentColumn + columnOffset, currentLine.Count);
+			return IdxToWord (currentLine, cursorPosition, columnOffset);
 		}
 
 		/// <inheritdoc/>

+ 234 - 0
UICatalog/Scenarios/Animation.cs

@@ -0,0 +1,234 @@
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.ColorSpaces;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Terminal.Gui;
+using Attribute = Terminal.Gui.Attribute;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "Animation", Description: "Demonstration of how to render animated images with threading.")]
+	[ScenarioCategory ("Colors")]
+	public class Animation : Scenario
+	{
+		private bool isDisposed;
+
+		public override void Setup ()
+		{
+			base.Setup ();
+
+
+			var imageView = new ImageView () {
+				Width = Dim.Fill(),
+				Height = Dim.Fill()-2,
+			};
+
+			Win.Add (imageView);
+
+			var lbl = new Label("Image by Wikiscient"){
+				Y = Pos.AnchorEnd(2)
+			};
+			Win.Add(lbl);
+
+			var lbl2 = new Label("https://commons.wikimedia.org/wiki/File:Spinning_globe.gif"){
+				Y = Pos.AnchorEnd(1)
+			};
+			Win.Add(lbl2);
+
+			var dir = new DirectoryInfo(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));
+			
+			var f = new FileInfo(
+				Path.Combine(dir.FullName,"Scenarios","Spinning_globe_dark_small.gif"));
+			if(!f.Exists)
+			{
+				MessageBox.ErrorQuery("Could not find gif","Could not find "+ f.FullName,"Ok");
+				return;
+			}
+
+			imageView.SetImage(Image.Load<Rgba32> (File.ReadAllBytes (f.FullName)));
+
+			Task.Run(()=>{
+				while(!isDisposed)
+				{
+					// When updating from a Thread/Task always use Invoke
+					Application.MainLoop.Invoke(()=>
+					{
+						imageView.NextFrame();
+						imageView.SetNeedsDisplay();
+					});
+
+					Task.Delay(100).Wait();
+				}
+			});
+		}
+
+		protected override void Dispose(bool disposing)
+		{
+			isDisposed = true;
+			base.Dispose();
+		}
+
+		// This is a C# port of https://github.com/andraaspar/bitmap-to-braille by Andraaspar
+
+		/// <summary>
+		/// Renders an image as unicode Braille.
+		/// </summary>
+		public class BitmapToBraille
+		{
+
+			public const int CHAR_WIDTH = 2;
+			public const int CHAR_HEIGHT = 4;
+
+			const string CHARS = " ⠁⠂⠃⠄⠅⠆⠇⡀⡁⡂⡃⡄⡅⡆⡇⠈⠉⠊⠋⠌⠍⠎⠏⡈⡉⡊⡋⡌⡍⡎⡏⠐⠑⠒⠓⠔⠕⠖⠗⡐⡑⡒⡓⡔⡕⡖⡗⠘⠙⠚⠛⠜⠝⠞⠟⡘⡙⡚⡛⡜⡝⡞⡟⠠⠡⠢⠣⠤⠥⠦⠧⡠⡡⡢⡣⡤⡥⡦⡧⠨⠩⠪⠫⠬⠭⠮⠯⡨⡩⡪⡫⡬⡭⡮⡯⠰⠱⠲⠳⠴⠵⠶⠷⡰⡱⡲⡳⡴⡵⡶⡷⠸⠹⠺⠻⠼⠽⠾⠿⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⣀⣁⣂⣃⣄⣅⣆⣇⢈⢉⢊⢋⢌⢍⢎⢏⣈⣉⣊⣋⣌⣍⣎⣏⢐⢑⢒⢓⢔⢕⢖⢗⣐⣑⣒⣓⣔⣕⣖⣗⢘⢙⢚⢛⢜⢝⢞⢟⣘⣙⣚⣛⣜⣝⣞⣟⢠⢡⢢⢣⢤⢥⢦⢧⣠⣡⣢⣣⣤⣥⣦⣧⢨⢩⢪⢫⢬⢭⢮⢯⣨⣩⣪⣫⣬⣭⣮⣯⢰⢱⢲⢳⢴⢵⢶⢷⣰⣱⣲⣳⣴⣵⣶⣷⢸⢹⢺⢻⢼⢽⢾⢿⣸⣹⣺⣻⣼⣽⣾⣿";
+
+			public int WidthPixels {get; }
+			public int HeightPixels { get; }
+
+			public Func<int,int,bool> PixelIsLit {get;}
+
+			public BitmapToBraille (int widthPixels, int heightPixels, Func<int, int, bool> pixelIsLit)
+			{
+				WidthPixels = widthPixels;
+				HeightPixels = heightPixels;
+				PixelIsLit = pixelIsLit;
+			}
+
+			public string GenerateImage() {
+				int imageHeightChars = (int) Math.Ceiling((double)HeightPixels / CHAR_HEIGHT);
+				int imageWidthChars = (int) Math.Ceiling((double)WidthPixels / CHAR_WIDTH);
+
+				var result = new StringBuilder();
+
+				for (int y = 0; y < imageHeightChars; y++) {
+					
+					for (int x = 0; x < imageWidthChars; x++) {
+						int baseX = x * CHAR_WIDTH;
+						int baseY = y * CHAR_HEIGHT;
+
+						int charIndex = 0;
+						int value = 1;
+
+						for (int charX = 0; charX < CHAR_WIDTH; charX++) {
+							for (int charY = 0; charY < CHAR_HEIGHT; charY++) {
+								int bitmapX = baseX + charX;
+								int bitmapY = baseY + charY;
+								bool pixelExists = bitmapX < WidthPixels && bitmapY < HeightPixels;
+
+								if (pixelExists && PixelIsLit(bitmapX, bitmapY)) {
+									charIndex += value;
+								}
+								value *= 2;
+							}
+						}
+
+						result.Append(CHARS[charIndex]);
+					}
+					result.Append('\n');
+				}
+				return result.ToString().TrimEnd();
+			}  
+		}
+
+		class ImageView : View {
+			private int frameCount;
+			private int currentFrame = 0;
+
+			private Image<Rgba32>[] fullResImages;
+			private Image<Rgba32>[] matchSizes;
+			private string[] brailleCache;
+
+			Rect oldSize = Rect.Empty;
+
+
+			internal void SetImage (Image<Rgba32> image)
+			{
+				frameCount = image.Frames.Count;
+
+				fullResImages = new Image<Rgba32>[frameCount];
+				matchSizes = new Image<Rgba32>[frameCount];
+				brailleCache = new string[frameCount];
+
+				for(int i=0;i<frameCount-1;i++)
+				{
+					fullResImages[i] = image.Frames.ExportFrame(0);
+				}
+				fullResImages[frameCount-1] = image;
+
+				this.SetNeedsDisplay ();
+			}
+			public void NextFrame()
+			{
+				currentFrame = (currentFrame+1)%frameCount;
+			}
+
+			public override void Redraw (Rect bounds)
+			{
+				base.Redraw (bounds);
+
+				if(oldSize != bounds)
+				{
+					// Invalidate cached images now size has changed
+					matchSizes = new Image<Rgba32>[frameCount];
+					brailleCache = new string[frameCount];
+					oldSize = bounds;
+				}
+
+				var imgScaled = matchSizes[currentFrame];
+				var braille = brailleCache[currentFrame];
+
+				if(imgScaled == null)
+				{
+					var imgFull = fullResImages[currentFrame];
+				
+					// keep aspect ratio
+					var newSize = Math.Min(bounds.Width,bounds.Height);
+
+					// generate one
+					matchSizes[currentFrame] = imgScaled = imgFull.Clone (
+						x => x.Resize (
+							 newSize * BitmapToBraille.CHAR_HEIGHT,
+							 newSize * BitmapToBraille.CHAR_HEIGHT));
+				}
+
+				if(braille == null)
+				{
+					brailleCache[currentFrame] = braille = GetBraille(matchSizes[currentFrame]);
+				}
+
+
+				var lines = braille.Split('\n');
+
+				for(int y = 0; y < lines.Length;y++)
+				{
+					var line = lines[y];
+					for(int x = 0;x<line.Length ;x++)
+					{
+						AddRune(x,y,line[x]);
+					}
+				}
+			}
+
+			private string GetBraille (Image<Rgba32> img)
+			{
+				var braille = new BitmapToBraille(
+					img.Width,
+					img.Height,
+					(x,y)=>IsLit(img,x,y));
+
+				return braille.GenerateImage();
+			}
+
+			private bool IsLit (Image<Rgba32> img, int x, int y)
+			{
+				var rgb = img[x,y];
+				return rgb.R + rgb.G + rgb.B > 50;
+			}
+		}
+	}
+}

+ 3 - 2
UICatalog/Scenarios/BasicColors.cs

@@ -13,13 +13,14 @@ namespace UICatalog.Scenarios {
 			var colors = System.Enum.GetValues (typeof (Color));
 
 			foreach (Color bg in colors) {
+				Attribute attr = new Attribute (bg, colors.Length - 1 - bg);
 				var vl = new Label (bg.ToString (), TextDirection.TopBottom_LeftRight) {
 					X = vx,
 					Y = 0,
 					Width = 1,
 					Height = 13,
 					VerticalTextAlignment = VerticalTextAlignment.Bottom,
-					ColorScheme = new ColorScheme () { Normal = new Attribute (bg, colors.Length - 1 - bg) }
+					ColorScheme = new ColorScheme () { Normal = attr }
 				};
 				Win.Add (vl);
 				var hl = new Label (bg.ToString ()) {
@@ -28,7 +29,7 @@ namespace UICatalog.Scenarios {
 					Width = 13,
 					Height = 1,
 					TextAlignment = TextAlignment.Right,
-					ColorScheme = new ColorScheme () { Normal = new Attribute (bg, colors.Length - 1 - bg) }
+					ColorScheme = new ColorScheme () { Normal = attr }
 				};
 				Win.Add (hl);
 				vx++;

+ 212 - 0
UICatalog/Scenarios/LineDrawing.cs

@@ -0,0 +1,212 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection.Metadata.Ecma335;
+using Terminal.Gui;
+using Terminal.Gui.Graphs;
+
+namespace UICatalog.Scenarios {
+
+	[ScenarioMetadata (Name: "Line Drawing", Description: "Demonstrates LineCanvas.")]
+	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Layout")]
+	public class LineDrawing : Scenario {
+
+		public override void Setup ()
+		{
+			var toolsWidth = 8;
+
+			var canvas = new DrawingArea {
+				Width = Dim.Fill (-toolsWidth),
+				Height = Dim.Fill ()
+			};
+
+			var tools = new ToolsView (toolsWidth) {
+				Y = 1,
+				X = Pos.AnchorEnd (toolsWidth + 1),
+				Height = Dim.Fill (),
+				Width = Dim.Fill ()
+			};
+
+
+			tools.ColorChanged += (c) => canvas.SetColor (c);
+			tools.SetStyle += (b) => canvas.BorderStyle = b;
+
+			Win.Add (canvas);
+			Win.Add (tools);
+			Win.Add (new Label (" -Tools-") { X = Pos.AnchorEnd (toolsWidth + 1) });
+		}
+
+		class ToolsView : View {
+
+			LineCanvas grid;
+			public event Action<Color> ColorChanged;
+			public event Action<BorderStyle> SetStyle;
+
+			Dictionary<Point, Color> swatches = new Dictionary<Point, Color> {
+				{ new Point(1,1),Color.Red},
+				{ new Point(3,1),Color.Green},
+				{ new Point(5,1),Color.BrightBlue},
+				{ new Point(7,1),Color.Black},
+				{ new Point(1,3),Color.White},
+			};
+
+			public ToolsView (int width)
+			{
+				grid = new LineCanvas ();
+
+				grid.AddLine (new Point (0, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+				grid.AddLine (new Point (0, 0), width, Orientation.Horizontal, BorderStyle.Single);
+				grid.AddLine (new Point (width, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+
+				grid.AddLine (new Point (0, 2), width, Orientation.Horizontal, BorderStyle.Single);
+
+				grid.AddLine (new Point (2, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+				grid.AddLine (new Point (4, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+				grid.AddLine (new Point (6, 0), int.MaxValue, Orientation.Vertical, BorderStyle.Single);
+
+				grid.AddLine (new Point (0, 4), width, Orientation.Horizontal, BorderStyle.Single);
+			}
+			public override void Redraw (Rect bounds)
+			{
+				base.Redraw (bounds);
+
+				Driver.SetAttribute (new Terminal.Gui.Attribute (Color.DarkGray, ColorScheme.Normal.Background));
+				
+				
+				foreach(var p in grid.GenerateImage(bounds))
+				{
+					this.AddRune(p.Key.X,p.Key.Y,p.Value);
+				}
+
+				foreach (var swatch in swatches) {
+					Driver.SetAttribute (new Terminal.Gui.Attribute (swatch.Value, ColorScheme.Normal.Background));
+					AddRune (swatch.Key.X, swatch.Key.Y, '█');
+				}
+
+				Driver.SetAttribute (new Terminal.Gui.Attribute (ColorScheme.Normal.Foreground, ColorScheme.Normal.Background));
+				AddRune (3, 3, Application.Driver.HDLine);
+				AddRune (5, 3, Application.Driver.HLine);
+				AddRune (7, 3, Application.Driver.ULRCorner);
+			}
+
+			public override bool OnMouseEvent (MouseEvent mouseEvent)
+			{
+				if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) {
+					foreach (var swatch in swatches) {
+						if (mouseEvent.X == swatch.Key.X && mouseEvent.Y == swatch.Key.Y) {
+
+							ColorChanged?.Invoke (swatch.Value);
+							return true;
+						}
+					}
+
+					if (mouseEvent.X == 3 && mouseEvent.Y == 3) {
+
+						SetStyle?.Invoke (BorderStyle.Double);
+						return true;
+					}
+					if (mouseEvent.X == 5 && mouseEvent.Y == 3) {
+
+						SetStyle?.Invoke (BorderStyle.Single);
+						return true;
+					}
+					if (mouseEvent.X == 7 && mouseEvent.Y == 3) {
+
+						SetStyle?.Invoke (BorderStyle.Rounded);
+						return true;
+					}
+				}
+
+				return base.OnMouseEvent (mouseEvent);
+			}
+		}
+
+		class DrawingArea : View {
+			/// <summary>
+			/// Index into <see cref="canvases"/> by color.
+			/// </summary>
+			Dictionary<Color, int> colorLayers = new Dictionary<Color, int> ();
+			List<LineCanvas> canvases = new List<LineCanvas> ();
+			int currentColor;
+
+			Point? currentLineStart = null;
+
+			public BorderStyle BorderStyle { get; internal set; }
+
+			public DrawingArea ()
+			{
+				AddCanvas (Color.White);
+			}
+
+			private void AddCanvas (Color c)
+			{
+				if (colorLayers.ContainsKey (c)) {
+					return;
+				}
+
+				canvases.Add (new LineCanvas ());
+				colorLayers.Add (c, canvases.Count - 1);
+				currentColor = canvases.Count - 1;
+			}
+
+			public override void Redraw (Rect bounds)
+			{
+				base.Redraw (bounds);
+
+				foreach (var kvp in colorLayers) {
+
+					Driver.SetAttribute (new Terminal.Gui.Attribute (kvp.Key, ColorScheme.Normal.Background));
+
+					var canvas = canvases [kvp.Value];
+
+					foreach(var p in canvas.GenerateImage(bounds))
+					{
+						this.AddRune(p.Key.X,p.Key.Y,p.Value);
+					}
+				}
+			}
+			public override bool OnMouseEvent (MouseEvent mouseEvent)
+			{
+
+				if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) {
+					if (currentLineStart == null) {
+						currentLineStart = new Point (mouseEvent.X, mouseEvent.Y);
+					}
+				} else {
+					if (currentLineStart != null) {
+
+						var start = currentLineStart.Value;
+						var end = new Point (mouseEvent.X, mouseEvent.Y);
+						var orientation = Orientation.Vertical;
+						var length = end.Y - start.Y;
+
+						// if line is wider than it is tall switch to horizontal
+						if (Math.Abs (start.X - end.X) > Math.Abs (start.Y - end.Y)) {
+							orientation = Orientation.Horizontal;
+							length = end.X - start.X;
+						}
+
+
+						canvases [currentColor].AddLine (
+							start,
+							length,
+							orientation,
+							BorderStyle);
+
+						currentLineStart = null;
+						SetNeedsDisplay ();
+					}
+				}
+
+				return base.OnMouseEvent (mouseEvent);
+			}
+
+			internal void SetColor (Color c)
+			{
+				AddCanvas (c);
+				currentColor = colorLayers [c];
+			}
+
+		}
+	}
+}

BIN
UICatalog/Scenarios/Spinning_globe_dark_small.gif


+ 3 - 0
UICatalog/Scenarios/spinning-globe-attribution.txt

@@ -0,0 +1,3 @@
+Author: Wikiscient
+Date: 16 September 2011
+Original Url: https://commons.wikimedia.org/wiki/File:Spinning_globe.gif

+ 1 - 1
UICatalog/UICatalog.cs

@@ -164,7 +164,7 @@ namespace UICatalog {
 
 			public UICatalogTopLevel ()
 			{
-				ColorScheme = _colorScheme;
+				ColorScheme = _colorScheme = Colors.Base;
 				MenuBar = new MenuBar (new MenuBarItem [] {
 					new MenuBarItem ("_File", new MenuItem [] {
 						new MenuItem ("_Quit", "Quit UI Catalog", () => RequestStop(), null, null, Key.Q | Key.CtrlMask)

+ 8 - 4
UICatalog/UICatalog.csproj

@@ -7,10 +7,10 @@
     <!-- Version numbers are automatically updated by gitversion when a release is released -->
     <!-- In the source tree the version will always be 2.0 for all projects. -->
     <!-- Do not modify these. -->
-    <AssemblyVersion>2.0</AssemblyVersion>
-    <FileVersion>2.0</FileVersion>
-    <Version>2.0</Version>
-    <InformationalVersion>2.0</InformationalVersion>
+    <AssemblyVersion>1.0</AssemblyVersion>
+    <FileVersion>1.0</FileVersion>
+    <Version>1.0</Version>
+    <InformationalVersion>1.0</InformationalVersion>
   </PropertyGroup>
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
     <DefineConstants>TRACE</DefineConstants>
@@ -19,6 +19,10 @@
     <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
   </PropertyGroup>
   <ItemGroup>
+  <None Update="./Scenarios/Spinning_globe_dark_small.gif" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
     <PackageReference Include="CsvHelper" Version="30.0.1" />
     <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="3.1.6" />
   </ItemGroup>

+ 0 - 1
UnitTests/Application/ApplicationTests.cs

@@ -24,7 +24,6 @@ namespace Terminal.Gui.ApplicationTests {
 			Assert.Null (Application.Driver);
 			Assert.Null (Application.Top);
 			Assert.Null (Application.Current);
-			Assert.Throws<ArgumentNullException> (() => Application.HeightAsBuffer == true);
 			Assert.Null (Application.MainLoop);
 			Assert.Null (Application.Iteration);
 			Assert.Null (Application.RootMouseEvent);

+ 346 - 0
UnitTests/Core/LineCanvasTests.cs

@@ -0,0 +1,346 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Terminal.Gui.Graphs;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.CoreTests {
+	public class LineCanvasTests {
+
+		readonly ITestOutputHelper output;
+
+		public LineCanvasTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Dot ()
+		{
+			var v = GetCanvas (out var canvas);
+			canvas.AddLine (new Point (0, 0), 0, Orientation.Horizontal, BorderStyle.Single);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+.";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		[InlineData (BorderStyle.Single)]
+		[InlineData (BorderStyle.Rounded)]
+		[Theory, AutoInitShutdown]
+		public void TestLineCanvas_Horizontal (BorderStyle style)
+		{
+			var v = GetCanvas (out var canvas);
+			canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, style);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+──";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Horizontal_Double ()
+		{
+			var v = GetCanvas (out var canvas);
+			canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Double);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@" 
+══";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		[InlineData (BorderStyle.Single)]
+		[InlineData (BorderStyle.Rounded)]
+		[Theory, AutoInitShutdown]
+		public void TestLineCanvas_Vertical (BorderStyle style)
+		{
+			var v = GetCanvas (out var canvas);
+			canvas.AddLine (new Point (0, 0), 1, Orientation.Vertical, style);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+│
+│";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Vertical_Double ()
+		{
+			var v = GetCanvas (out var canvas);
+			canvas.AddLine (new Point (0, 0), 1, Orientation.Vertical, BorderStyle.Double);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+║
+║";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		/// <summary>
+		/// This test demonstrates that corners are only drawn when lines overlap.
+		/// Not when they terminate adjacent to one another.
+		/// </summary>
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Corner_NoOverlap ()
+		{
+			var v = GetCanvas (out var canvas);
+			canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Single);
+			canvas.AddLine (new Point (0, 1), 1, Orientation.Vertical, BorderStyle.Single);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+──
+│
+│";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+		/// <summary>
+		/// This test demonstrates how to correctly trigger a corner.  By
+		/// overlapping the lines in the same cell
+		/// </summary>
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Corner_Correct ()
+		{
+			var v = GetCanvas (out var canvas);
+			canvas.AddLine (new Point (0, 0), 1, Orientation.Horizontal, BorderStyle.Single);
+			canvas.AddLine (new Point (0, 0), 2, Orientation.Vertical, BorderStyle.Single);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+┌─
+│
+│";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Window ()
+		{
+			var v = GetCanvas (out var canvas);
+
+			// outer box
+			canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Single);
+			canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single);
+			canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Single);
+			canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Single);
+
+
+			canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Single);
+			canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Single);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+┌────┬───┐
+│    │   │
+├────┼───┤
+│    │   │
+└────┴───┘";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		/// <summary>
+		/// Demonstrates when <see cref="BorderStyle.Rounded"/> corners are used. Notice how
+		/// not all lines declare rounded.  If there are 1+ lines intersecting and a corner is
+		/// to be used then if any of them are rounded a rounded corner is used.
+		/// </summary>
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Window_Rounded ()
+		{
+			var v = GetCanvas (out var canvas);
+
+			// outer box
+			canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Rounded);
+
+			// BorderStyle.Single is ignored because corner overlaps with the above line which is Rounded
+			// this results in a rounded corner being used.
+			canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Single);
+			canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Rounded);
+			canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Single);
+
+			// These lines say rounded but they will result in the T sections which are never rounded.
+			canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Rounded);
+			canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Rounded);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+╭────┬───╮
+│    │   │
+├────┼───┤
+│    │   │
+╰────┴───╯";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_Window_Double ()
+		{
+			var v = GetCanvas (out var canvas);
+
+			// outer box
+			canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Double);
+			canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Double);
+			canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Double);
+			canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Double);
+
+
+			canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Double);
+			canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Double);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+╔════╦═══╗
+║    ║   ║
+╠════╬═══╣
+║    ║   ║
+╚════╩═══╝";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+
+		[Theory, AutoInitShutdown]
+		[InlineData (BorderStyle.Single)]
+		[InlineData (BorderStyle.Rounded)]
+		public void TestLineCanvas_Window_DoubleTop_SingleSides (BorderStyle thinStyle)
+		{
+			var v = GetCanvas (out var canvas);
+
+			// outer box
+			canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, BorderStyle.Double);
+			canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, thinStyle);
+			canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, BorderStyle.Double);
+			canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, thinStyle);
+
+
+			canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, thinStyle);
+			canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, BorderStyle.Double);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+╒════╤═══╕
+│    │   │
+╞════╪═══╡
+│    │   │
+╘════╧═══╛
+";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		[Theory, AutoInitShutdown]
+		[InlineData (BorderStyle.Single)]
+		[InlineData (BorderStyle.Rounded)]
+		public void TestLineCanvas_Window_SingleTop_DoubleSides (BorderStyle thinStyle)
+		{
+			var v = GetCanvas (out var canvas);
+
+			// outer box
+			canvas.AddLine (new Point (0, 0), 9, Orientation.Horizontal, thinStyle);
+			canvas.AddLine (new Point (9, 0), 4, Orientation.Vertical, BorderStyle.Double);
+			canvas.AddLine (new Point (9, 4), -9, Orientation.Horizontal, thinStyle);
+			canvas.AddLine (new Point (0, 4), -4, Orientation.Vertical, BorderStyle.Double);
+
+
+			canvas.AddLine (new Point (5, 0), 4, Orientation.Vertical, BorderStyle.Double);
+			canvas.AddLine (new Point (0, 2), 9, Orientation.Horizontal, thinStyle);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"    
+╓────╥───╖
+║    ║   ║
+╟────╫───╢
+║    ║   ║
+╙────╨───╜
+
+";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestLineCanvas_LeaveMargin_Top1_Left1 ()
+		{
+			// Draw at 1,1 within client area of View (i.e. leave a top and left margin of 1)
+			var v = GetCanvas (out var canvas, 1, 1);
+
+			// outer box
+			canvas.AddLine (new Point (0, 0), 8, Orientation.Horizontal, BorderStyle.Single);
+			canvas.AddLine (new Point (8, 0), 3, Orientation.Vertical, BorderStyle.Single);
+			canvas.AddLine (new Point (8, 3), -8, Orientation.Horizontal, BorderStyle.Single);
+			canvas.AddLine (new Point (0, 3), -3, Orientation.Vertical, BorderStyle.Single);
+
+
+			canvas.AddLine (new Point (5, 0), 3, Orientation.Vertical, BorderStyle.Single);
+			canvas.AddLine (new Point (0, 2), 8, Orientation.Horizontal, BorderStyle.Single);
+
+			v.Redraw (v.Bounds);
+
+			string looksLike =
+@"
+ ┌────┬──┐
+ │    │  │
+ ├────┼──┤
+ └────┴──┘
+";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
+
+
+		/// <summary>
+		/// Creates a new <see cref="View"/> into which a <see cref="LineCanvas"/> is rendered
+		/// at <see cref="View.DrawContentComplete"/> time.
+		/// </summary>
+		/// <param name="canvas">The <see cref="LineCanvas"/> you can draw into.</param>
+		/// <param name="offsetX">How far to offset drawing in X</param>
+		/// <param name="offsetY">How far to offset drawing in Y</param>
+		/// <returns></returns>
+		private View GetCanvas (out LineCanvas canvas, int offsetX = 0, int offsetY = 0)
+		{
+			var v = new View {
+				Width = 10,
+				Height = 5,
+				Bounds = new Rect (0, 0, 10, 5)
+			};
+
+			var canvasCopy = canvas = new LineCanvas ();
+			v.DrawContentComplete += (r) => {
+					foreach(var p in canvasCopy.GenerateImage(v.Bounds))
+					{
+						v.AddRune(
+							offsetX + p.Key.X,
+							offsetY + p.Key.Y,
+							p.Value);
+					}
+				};
+
+			return v;
+		}
+	}
+}

+ 69 - 5
UnitTests/Drivers/AttributeTests.cs

@@ -85,7 +85,34 @@ namespace Terminal.Gui.DriverTests {
 		}
 
 		[Fact]
-		public void Make_Asserts_IfNotInit ()
+		public void Implicit_Assign_NoDriver ()
+		{
+
+			var attr = new Attribute ();
+
+			var fg = new Color ();
+			fg = Color.Red;
+
+			var bg = new Color ();
+			bg = Color.Blue;
+
+			// Test conversion to int
+			attr = new Attribute (fg, bg);
+			int value_implicit = (int)attr.Value;
+			Assert.False (attr.Initialized);
+
+			Assert.Equal (-1, value_implicit);
+			Assert.False (attr.Initialized);
+
+			// Test conversion from int
+			attr = -1;
+			Assert.Equal (-1, attr.Value);
+			Assert.False (attr.Initialized);
+
+		}
+
+		[Fact]
+		public void Make_SetsNotInitialized_NoDriver ()
 		{
 			var fg = new Color ();
 			fg = Color.Red;
@@ -93,7 +120,9 @@ namespace Terminal.Gui.DriverTests {
 			var bg = new Color ();
 			bg = Color.Blue;
 
-			Assert.Throws<InvalidOperationException> (() => Attribute.Make (fg, bg));
+			var a = Attribute.Make (fg, bg);
+
+			Assert.False (a.Initialized);
 		}
 
 		[Fact]
@@ -109,8 +138,8 @@ namespace Terminal.Gui.DriverTests {
 			var bg = new Color ();
 			bg = Color.Blue;
 
-			var attr =  Attribute.Make (fg, bg);
-
+			var attr = Attribute.Make (fg, bg);
+			Assert.True (attr.Initialized);
 			Assert.Equal (fg, attr.Foreground);
 			Assert.Equal (bg, attr.Background);
 
@@ -119,7 +148,23 @@ namespace Terminal.Gui.DriverTests {
 		}
 
 		[Fact]
-		public void Get_Asserts_IfNotInit ()
+		public void Make_Creates_NoDriver ()
+		{
+
+			var fg = new Color ();
+			fg = Color.Red;
+
+			var bg = new Color ();
+			bg = Color.Blue;
+
+			var attr = Attribute.Make (fg, bg);
+			Assert.False (attr.Initialized);
+			Assert.Equal (fg, attr.Foreground);
+			Assert.Equal (bg, attr.Background);
+		}
+
+		[Fact]
+		public void Get_Asserts_NoDriver ()
 		{
 			Assert.Throws<InvalidOperationException> (() => Attribute.Get ());
 		}
@@ -163,5 +208,24 @@ namespace Terminal.Gui.DriverTests {
 			Assert.Equal (Color.Red, fg);
 			Assert.Equal (Color.Green, bg);
 		}
+
+		[Fact]
+		public void IsValid_Tests ()
+		{
+			var attr = new Attribute ();
+			Assert.True (attr.HasValidColors);
+
+			attr = new Attribute (Color.Red, Color.Green);
+			Assert.True (attr.HasValidColors);
+
+			attr = new Attribute (Color.Red, (Color)(-1));
+			Assert.False (attr.HasValidColors);
+
+			attr = new Attribute ((Color)(-1), Color.Green);
+			Assert.False (attr.HasValidColors);
+
+			attr = new Attribute ((Color)(-1), (Color)(-1));
+			Assert.False (attr.HasValidColors);
+		}
 	}
 }

+ 38 - 0
UnitTests/Drivers/ColorTests.cs

@@ -35,5 +35,43 @@ namespace Terminal.Gui.DriverTests {
 			Application.Shutdown ();
 		}
 
+		[Fact, AutoInitShutdown]
+		public void ColorScheme_New ()
+		{
+			var scheme = new ColorScheme ();
+			var lbl = new Label ();
+			lbl.ColorScheme = scheme;
+			lbl.Redraw (lbl.Bounds);
+		}
+
+		[Fact]
+		public void TestAllColors ()
+		{
+			var colors = System.Enum.GetValues (typeof (Color));
+			Attribute [] attrs = new Attribute [colors.Length];
+
+			int idx = 0;
+			foreach (Color bg in colors) {
+				attrs [idx] = new Attribute (bg, colors.Length - 1 - bg);
+				idx++;
+			}
+			Assert.Equal (16, attrs.Length);
+			Assert.Equal (new Attribute (Color.Black, Color.White), attrs [0]);
+			Assert.Equal (new Attribute (Color.Blue, Color.BrightYellow), attrs [1]);
+			Assert.Equal (new Attribute (Color.Green, Color.BrightMagenta), attrs [2]);
+			Assert.Equal (new Attribute (Color.Cyan, Color.BrightRed), attrs [3]);
+			Assert.Equal (new Attribute (Color.Red, Color.BrightCyan), attrs [4]);
+			Assert.Equal (new Attribute (Color.Magenta, Color.BrightGreen), attrs [5]);
+			Assert.Equal (new Attribute (Color.Brown, Color.BrightBlue), attrs [6]);
+			Assert.Equal (new Attribute (Color.Gray, Color.DarkGray), attrs [7]);
+			Assert.Equal (new Attribute (Color.DarkGray, Color.Gray), attrs [8]);
+			Assert.Equal (new Attribute (Color.BrightBlue, Color.Brown), attrs [9]);
+			Assert.Equal (new Attribute (Color.BrightGreen, Color.Magenta), attrs [10]);
+			Assert.Equal (new Attribute (Color.BrightCyan, Color.Red), attrs [11]);
+			Assert.Equal (new Attribute (Color.BrightRed, Color.Cyan), attrs [12]);
+			Assert.Equal (new Attribute (Color.BrightMagenta, Color.Green), attrs [13]);
+			Assert.Equal (new Attribute (Color.BrightYellow, Color.Blue), attrs [14]);
+			Assert.Equal (new Attribute (Color.White, Color.Black), attrs [^1]);
+		}
 	}
 }

+ 118 - 111
UnitTests/Drivers/ConsoleDriverTests.cs

@@ -440,6 +440,8 @@ namespace Terminal.Gui.DriverTests {
 			Assert.Equal (code, actual.Value);
 		}
 
+		private static object packetLock = new object ();
+		
 		/// <summary>
 		/// Sometimes when using remote tools EventKeyRecord sends 'virtual keystrokes'.
 		/// These are indicated with the wVirtualKeyCode of 231. When we see this code
@@ -485,122 +487,127 @@ namespace Terminal.Gui.DriverTests {
 				if (iterations == 0) Application.Driver.SendKeys ((char)mappedConsoleKey, ConsoleKey.Packet, shift, alt, control);
 			};
 
-			Application.Run ();
-			Application.Shutdown ();
+
+			lock (packetLock) {
+				Application.Run ();
+				Application.Shutdown ();
+			}
 		}
 
 		public class PacketTest : IEnumerable, IEnumerable<object []> {
 			public IEnumerator<object []> GetEnumerator ()
 			{
-				yield return new object [] { 'a', false, false, false, 'A', 30, Key.a, 'A', 30 };
-				yield return new object [] { 'A', true, false, false, 'A', 30, Key.A | Key.ShiftMask, 'A', 30 };
-				yield return new object [] { 'A', true, true, false, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask, 'A', 30 };
-				yield return new object [] { 'A', true, true, true, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'A', 30 };
-				yield return new object [] { 'z', false, false, false, 'Z', 44, Key.z, 'Z', 44 };
-				yield return new object [] { 'Z', true, false, false, 'Z', 44, Key.Z | Key.ShiftMask, 'Z', 44 };
-				yield return new object [] { 'Z', true, true, false, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask, 'Z', 44 };
-				yield return new object [] { 'Z', true, true, true, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'Z', 44 };
-				yield return new object [] { '英', false, false, false, '\0', 0, (Key)'英', '\0', 0 };
-				yield return new object [] { '英', true, false, false, '\0', 0, (Key)'英' | Key.ShiftMask, '\0', 0 };
-				yield return new object [] { '英', true, true, false, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask, '\0', 0 };
-				yield return new object [] { '英', true, true, true, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '\0', 0 };
-				yield return new object [] { '+', false, false, false, 187, 26, (Key)'+', 187, 26 };
-				yield return new object [] { '*', true, false, false, 187, 26, (Key)'*' | Key.ShiftMask, 187, 26 };
-				yield return new object [] { '+', true, true, false, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask, 187, 26 };
-				yield return new object [] { '+', true, true, true, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 187, 26 };
-				yield return new object [] { '1', false, false, false, '1', 2, Key.D1, '1', 2 };
-				yield return new object [] { '!', true, false, false, '1', 2, (Key)'!' | Key.ShiftMask, '1', 2 };
-				yield return new object [] { '1', true, true, false, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask, '1', 2 };
-				yield return new object [] { '1', true, true, true, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '1', 2 };
-				yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 };
-				yield return new object [] { '2', false, false, false, '2', 3, Key.D2, '2', 3 };
-				yield return new object [] { '"', true, false, false, '2', 3, (Key)'"' | Key.ShiftMask, '2', 3 };
-				yield return new object [] { '2', true, true, false, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask, '2', 3 };
-				yield return new object [] { '2', true, true, true, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '2', 3 };
-				yield return new object [] { '@', false, true, true, '2', 3, (Key)'@' | Key.AltMask | Key.CtrlMask, '2', 3 };
-				yield return new object [] { '3', false, false, false, '3', 4, Key.D3, '3', 4 };
-				yield return new object [] { '#', true, false, false, '3', 4, (Key)'#' | Key.ShiftMask, '3', 4 };
-				yield return new object [] { '3', true, true, false, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask, '3', 4 };
-				yield return new object [] { '3', true, true, true, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '3', 4 };
-				yield return new object [] { '£', false, true, true, '3', 4, (Key)'£' | Key.AltMask | Key.CtrlMask, '3', 4 };
-				yield return new object [] { '4', false, false, false, '4', 5, Key.D4, '4', 5 };
-				yield return new object [] { '$', true, false, false, '4', 5, (Key)'$' | Key.ShiftMask, '4', 5 };
-				yield return new object [] { '4', true, true, false, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask, '4', 5 };
-				yield return new object [] { '4', true, true, true, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '4', 5 };
-				yield return new object [] { '§', false, true, true, '4', 5, (Key)'§' | Key.AltMask | Key.CtrlMask, '4', 5 };
-				yield return new object [] { '5', false, false, false, '5', 6, Key.D5, '5', 6 };
-				yield return new object [] { '%', true, false, false, '5', 6, (Key)'%' | Key.ShiftMask, '5', 6 };
-				yield return new object [] { '5', true, true, false, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask, '5', 6 };
-				yield return new object [] { '5', true, true, true, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '5', 6 };
-				yield return new object [] { '€', false, true, true, '5', 6, (Key)'€' | Key.AltMask | Key.CtrlMask, '5', 6 };
-				yield return new object [] { '6', false, false, false, '6', 7, Key.D6, '6', 7 };
-				yield return new object [] { '&', true, false, false, '6', 7, (Key)'&' | Key.ShiftMask, '6', 7 };
-				yield return new object [] { '6', true, true, false, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask, '6', 7 };
-				yield return new object [] { '6', true, true, true, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '6', 7 };
-				yield return new object [] { '6', false, true, true, '6', 7, Key.D6 | Key.AltMask | Key.CtrlMask, '6', 7 };
-				yield return new object [] { '7', false, false, false, '7', 8, Key.D7, '7', 8 };
-				yield return new object [] { '/', true, false, false, '7', 8, (Key)'/' | Key.ShiftMask, '7', 8 };
-				yield return new object [] { '7', true, true, false, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask, '7', 8 };
-				yield return new object [] { '7', true, true, true, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '7', 8 };
-				yield return new object [] { '{', false, true, true, '7', 8, (Key)'{' | Key.AltMask | Key.CtrlMask, '7', 8 };
-				yield return new object [] { '8', false, false, false, '8', 9, Key.D8, '8', 9 };
-				yield return new object [] { '(', true, false, false, '8', 9, (Key)'(' | Key.ShiftMask, '8', 9 };
-				yield return new object [] { '8', true, true, false, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask, '8', 9 };
-				yield return new object [] { '8', true, true, true, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '8', 9 };
-				yield return new object [] { '[', false, true, true, '8', 9, (Key)'[' | Key.AltMask | Key.CtrlMask, '8', 9 };
-				yield return new object [] { '9', false, false, false, '9', 10, Key.D9, '9', 10 };
-				yield return new object [] { ')', true, false, false, '9', 10, (Key)')' | Key.ShiftMask, '9', 10 };
-				yield return new object [] { '9', true, true, false, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask, '9', 10 };
-				yield return new object [] { '9', true, true, true, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '9', 10 };
-				yield return new object [] { ']', false, true, true, '9', 10, (Key)']' | Key.AltMask | Key.CtrlMask, '9', 10 };
-				yield return new object [] { '0', false, false, false, '0', 11, Key.D0, '0', 11 };
-				yield return new object [] { '=', true, false, false, '0', 11, (Key)'=' | Key.ShiftMask, '0', 11 };
-				yield return new object [] { '0', true, true, false, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask, '0', 11 };
-				yield return new object [] { '0', true, true, true, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '0', 11 };
-				yield return new object [] { '}', false, true, true, '0', 11, (Key)'}' | Key.AltMask | Key.CtrlMask, '0', 11 };
-				yield return new object [] { '\'', false, false, false, 219, 12, (Key)'\'', 219, 12 };
-				yield return new object [] { '?', true, false, false, 219, 12, (Key)'?' | Key.ShiftMask, 219, 12 };
-				yield return new object [] { '\'', true, true, false, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask, 219, 12 };
-				yield return new object [] { '\'', true, true, true, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 219, 12 };
-				yield return new object [] { '«', false, false, false, 221, 13, (Key)'«', 221, 13 };
-				yield return new object [] { '»', true, false, false, 221, 13, (Key)'»' | Key.ShiftMask, 221, 13 };
-				yield return new object [] { '«', true, true, false, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask, 221, 13 };
-				yield return new object [] { '«', true, true, true, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 221, 13 };
-				yield return new object [] { 'á', false, false, false, 'á', 0, (Key)'á', 'A', 30 };
-				yield return new object [] { 'Á', true, false, false, 'Á', 0, (Key)'Á' | Key.ShiftMask, 'A', 30 };
-				yield return new object [] { 'à', false, false, false, 'à', 0, (Key)'à', 'A', 30 };
-				yield return new object [] { 'À', true, false, false, 'À', 0, (Key)'À' | Key.ShiftMask, 'A', 30 };
-				yield return new object [] { 'é', false, false, false, 'é', 0, (Key)'é', 'E', 18 };
-				yield return new object [] { 'É', true, false, false, 'É', 0, (Key)'É' | Key.ShiftMask, 'E', 18 };
-				yield return new object [] { 'è', false, false, false, 'è', 0, (Key)'è', 'E', 18 };
-				yield return new object [] { 'È', true, false, false, 'È', 0, (Key)'È' | Key.ShiftMask, 'E', 18 };
-				yield return new object [] { 'í', false, false, false, 'í', 0, (Key)'í', 'I', 23 };
-				yield return new object [] { 'Í', true, false, false, 'Í', 0, (Key)'Í' | Key.ShiftMask, 'I', 23 };
-				yield return new object [] { 'ì', false, false, false, 'ì', 0, (Key)'ì', 'I', 23 };
-				yield return new object [] { 'Ì', true, false, false, 'Ì', 0, (Key)'Ì' | Key.ShiftMask, 'I', 23 };
-				yield return new object [] { 'ó', false, false, false, 'ó', 0, (Key)'ó', 'O', 24 };
-				yield return new object [] { 'Ó', true, false, false, 'Ó', 0, (Key)'Ó' | Key.ShiftMask, 'O', 24 };
-				yield return new object [] { 'ò', false, false, false, 'Ó', 0, (Key)'ò', 'O', 24 };
-				yield return new object [] { 'Ò', true, false, false, 'Ò', 0, (Key)'Ò' | Key.ShiftMask, 'O', 24 };
-				yield return new object [] { 'ú', false, false, false, 'ú', 0, (Key)'ú', 'U', 22 };
-				yield return new object [] { 'Ú', true, false, false, 'Ú', 0, (Key)'Ú' | Key.ShiftMask, 'U', 22 };
-				yield return new object [] { 'ù', false, false, false, 'ù', 0, (Key)'ù', 'U', 22 };
-				yield return new object [] { 'Ù', true, false, false, 'Ù', 0, (Key)'Ù' | Key.ShiftMask, 'U', 22 };
-				yield return new object [] { 'ö', false, false, false, 'ó', 0, (Key)'ö', 'O', 24 };
-				yield return new object [] { 'Ö', true, false, false, 'Ó', 0, (Key)'Ö' | Key.ShiftMask, 'O', 24 };
-				yield return new object [] { '<', false, false, false, 226, 86, (Key)'<', 226, 86 };
-				yield return new object [] { '>', true, false, false, 226, 86, (Key)'>' | Key.ShiftMask, 226, 86 };
-				yield return new object [] { '<', true, true, false, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask, 226, 86 };
-				yield return new object [] { '<', true, true, true, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 226, 86 };
-				yield return new object [] { 'ç', false, false, false, 192, 39, (Key)'ç', 192, 39 };
-				yield return new object [] { 'Ç', true, false, false, 192, 39, (Key)'Ç' | Key.ShiftMask, 192, 39 };
-				yield return new object [] { 'ç', true, true, false, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask, 192, 39 };
-				yield return new object [] { 'ç', true, true, true, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 192, 39 };
-				yield return new object [] { '¨', false, true, true, 187, 26, (Key)'¨' | Key.AltMask | Key.CtrlMask, 187, 26 };
-				yield return new object [] { (uint)Key.PageUp, false, false, false, 33, 73, Key.PageUp, 33, 73 };
-				yield return new object [] { (uint)Key.PageUp, true, false, false, 33, 73, Key.PageUp | Key.ShiftMask, 33, 73 };
-				yield return new object [] { (uint)Key.PageUp, true, true, false, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask, 33, 73 };
-				yield return new object [] { (uint)Key.PageUp, true, true, true, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 33, 73 };
+				lock (packetLock) {
+					yield return new object [] { 'a', false, false, false, 'A', 30, Key.a, 'A', 30 };
+					yield return new object [] { 'A', true, false, false, 'A', 30, Key.A | Key.ShiftMask, 'A', 30 };
+					yield return new object [] { 'A', true, true, false, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask, 'A', 30 };
+					yield return new object [] { 'A', true, true, true, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'A', 30 };
+					yield return new object [] { 'z', false, false, false, 'Z', 44, Key.z, 'Z', 44 };
+					yield return new object [] { 'Z', true, false, false, 'Z', 44, Key.Z | Key.ShiftMask, 'Z', 44 };
+					yield return new object [] { 'Z', true, true, false, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask, 'Z', 44 };
+					yield return new object [] { 'Z', true, true, true, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'Z', 44 };
+					yield return new object [] { '英', false, false, false, '\0', 0, (Key)'英', '\0', 0 };
+					yield return new object [] { '英', true, false, false, '\0', 0, (Key)'英' | Key.ShiftMask, '\0', 0 };
+					yield return new object [] { '英', true, true, false, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask, '\0', 0 };
+					yield return new object [] { '英', true, true, true, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '\0', 0 };
+					yield return new object [] { '+', false, false, false, 187, 26, (Key)'+', 187, 26 };
+					yield return new object [] { '*', true, false, false, 187, 26, (Key)'*' | Key.ShiftMask, 187, 26 };
+					yield return new object [] { '+', true, true, false, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask, 187, 26 };
+					yield return new object [] { '+', true, true, true, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 187, 26 };
+					yield return new object [] { '1', false, false, false, '1', 2, Key.D1, '1', 2 };
+					yield return new object [] { '!', true, false, false, '1', 2, (Key)'!' | Key.ShiftMask, '1', 2 };
+					yield return new object [] { '1', true, true, false, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask, '1', 2 };
+					yield return new object [] { '1', true, true, true, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '1', 2 };
+					yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 };
+					yield return new object [] { '2', false, false, false, '2', 3, Key.D2, '2', 3 };
+					yield return new object [] { '"', true, false, false, '2', 3, (Key)'"' | Key.ShiftMask, '2', 3 };
+					yield return new object [] { '2', true, true, false, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask, '2', 3 };
+					yield return new object [] { '2', true, true, true, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '2', 3 };
+					yield return new object [] { '@', false, true, true, '2', 3, (Key)'@' | Key.AltMask | Key.CtrlMask, '2', 3 };
+					yield return new object [] { '3', false, false, false, '3', 4, Key.D3, '3', 4 };
+					yield return new object [] { '#', true, false, false, '3', 4, (Key)'#' | Key.ShiftMask, '3', 4 };
+					yield return new object [] { '3', true, true, false, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask, '3', 4 };
+					yield return new object [] { '3', true, true, true, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '3', 4 };
+					yield return new object [] { '£', false, true, true, '3', 4, (Key)'£' | Key.AltMask | Key.CtrlMask, '3', 4 };
+					yield return new object [] { '4', false, false, false, '4', 5, Key.D4, '4', 5 };
+					yield return new object [] { '$', true, false, false, '4', 5, (Key)'$' | Key.ShiftMask, '4', 5 };
+					yield return new object [] { '4', true, true, false, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask, '4', 5 };
+					yield return new object [] { '4', true, true, true, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '4', 5 };
+					yield return new object [] { '§', false, true, true, '4', 5, (Key)'§' | Key.AltMask | Key.CtrlMask, '4', 5 };
+					yield return new object [] { '5', false, false, false, '5', 6, Key.D5, '5', 6 };
+					yield return new object [] { '%', true, false, false, '5', 6, (Key)'%' | Key.ShiftMask, '5', 6 };
+					yield return new object [] { '5', true, true, false, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask, '5', 6 };
+					yield return new object [] { '5', true, true, true, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '5', 6 };
+					yield return new object [] { '€', false, true, true, '5', 6, (Key)'€' | Key.AltMask | Key.CtrlMask, '5', 6 };
+					yield return new object [] { '6', false, false, false, '6', 7, Key.D6, '6', 7 };
+					yield return new object [] { '&', true, false, false, '6', 7, (Key)'&' | Key.ShiftMask, '6', 7 };
+					yield return new object [] { '6', true, true, false, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask, '6', 7 };
+					yield return new object [] { '6', true, true, true, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '6', 7 };
+					yield return new object [] { '6', false, true, true, '6', 7, Key.D6 | Key.AltMask | Key.CtrlMask, '6', 7 };
+					yield return new object [] { '7', false, false, false, '7', 8, Key.D7, '7', 8 };
+					yield return new object [] { '/', true, false, false, '7', 8, (Key)'/' | Key.ShiftMask, '7', 8 };
+					yield return new object [] { '7', true, true, false, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask, '7', 8 };
+					yield return new object [] { '7', true, true, true, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '7', 8 };
+					yield return new object [] { '{', false, true, true, '7', 8, (Key)'{' | Key.AltMask | Key.CtrlMask, '7', 8 };
+					yield return new object [] { '8', false, false, false, '8', 9, Key.D8, '8', 9 };
+					yield return new object [] { '(', true, false, false, '8', 9, (Key)'(' | Key.ShiftMask, '8', 9 };
+					yield return new object [] { '8', true, true, false, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask, '8', 9 };
+					yield return new object [] { '8', true, true, true, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '8', 9 };
+					yield return new object [] { '[', false, true, true, '8', 9, (Key)'[' | Key.AltMask | Key.CtrlMask, '8', 9 };
+					yield return new object [] { '9', false, false, false, '9', 10, Key.D9, '9', 10 };
+					yield return new object [] { ')', true, false, false, '9', 10, (Key)')' | Key.ShiftMask, '9', 10 };
+					yield return new object [] { '9', true, true, false, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask, '9', 10 };
+					yield return new object [] { '9', true, true, true, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '9', 10 };
+					yield return new object [] { ']', false, true, true, '9', 10, (Key)']' | Key.AltMask | Key.CtrlMask, '9', 10 };
+					yield return new object [] { '0', false, false, false, '0', 11, Key.D0, '0', 11 };
+					yield return new object [] { '=', true, false, false, '0', 11, (Key)'=' | Key.ShiftMask, '0', 11 };
+					yield return new object [] { '0', true, true, false, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask, '0', 11 };
+					yield return new object [] { '0', true, true, true, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '0', 11 };
+					yield return new object [] { '}', false, true, true, '0', 11, (Key)'}' | Key.AltMask | Key.CtrlMask, '0', 11 };
+					yield return new object [] { '\'', false, false, false, 219, 12, (Key)'\'', 219, 12 };
+					yield return new object [] { '?', true, false, false, 219, 12, (Key)'?' | Key.ShiftMask, 219, 12 };
+					yield return new object [] { '\'', true, true, false, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask, 219, 12 };
+					yield return new object [] { '\'', true, true, true, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 219, 12 };
+					yield return new object [] { '«', false, false, false, 221, 13, (Key)'«', 221, 13 };
+					yield return new object [] { '»', true, false, false, 221, 13, (Key)'»' | Key.ShiftMask, 221, 13 };
+					yield return new object [] { '«', true, true, false, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask, 221, 13 };
+					yield return new object [] { '«', true, true, true, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 221, 13 };
+					yield return new object [] { 'á', false, false, false, 'á', 0, (Key)'á', 'A', 30 };
+					yield return new object [] { 'Á', true, false, false, 'Á', 0, (Key)'Á' | Key.ShiftMask, 'A', 30 };
+					yield return new object [] { 'à', false, false, false, 'à', 0, (Key)'à', 'A', 30 };
+					yield return new object [] { 'À', true, false, false, 'À', 0, (Key)'À' | Key.ShiftMask, 'A', 30 };
+					yield return new object [] { 'é', false, false, false, 'é', 0, (Key)'é', 'E', 18 };
+					yield return new object [] { 'É', true, false, false, 'É', 0, (Key)'É' | Key.ShiftMask, 'E', 18 };
+					yield return new object [] { 'è', false, false, false, 'è', 0, (Key)'è', 'E', 18 };
+					yield return new object [] { 'È', true, false, false, 'È', 0, (Key)'È' | Key.ShiftMask, 'E', 18 };
+					yield return new object [] { 'í', false, false, false, 'í', 0, (Key)'í', 'I', 23 };
+					yield return new object [] { 'Í', true, false, false, 'Í', 0, (Key)'Í' | Key.ShiftMask, 'I', 23 };
+					yield return new object [] { 'ì', false, false, false, 'ì', 0, (Key)'ì', 'I', 23 };
+					yield return new object [] { 'Ì', true, false, false, 'Ì', 0, (Key)'Ì' | Key.ShiftMask, 'I', 23 };
+					yield return new object [] { 'ó', false, false, false, 'ó', 0, (Key)'ó', 'O', 24 };
+					yield return new object [] { 'Ó', true, false, false, 'Ó', 0, (Key)'Ó' | Key.ShiftMask, 'O', 24 };
+					yield return new object [] { 'ò', false, false, false, 'Ó', 0, (Key)'ò', 'O', 24 };
+					yield return new object [] { 'Ò', true, false, false, 'Ò', 0, (Key)'Ò' | Key.ShiftMask, 'O', 24 };
+					yield return new object [] { 'ú', false, false, false, 'ú', 0, (Key)'ú', 'U', 22 };
+					yield return new object [] { 'Ú', true, false, false, 'Ú', 0, (Key)'Ú' | Key.ShiftMask, 'U', 22 };
+					yield return new object [] { 'ù', false, false, false, 'ù', 0, (Key)'ù', 'U', 22 };
+					yield return new object [] { 'Ù', true, false, false, 'Ù', 0, (Key)'Ù' | Key.ShiftMask, 'U', 22 };
+					yield return new object [] { 'ö', false, false, false, 'ó', 0, (Key)'ö', 'O', 24 };
+					yield return new object [] { 'Ö', true, false, false, 'Ó', 0, (Key)'Ö' | Key.ShiftMask, 'O', 24 };
+					yield return new object [] { '<', false, false, false, 226, 86, (Key)'<', 226, 86 };
+					yield return new object [] { '>', true, false, false, 226, 86, (Key)'>' | Key.ShiftMask, 226, 86 };
+					yield return new object [] { '<', true, true, false, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask, 226, 86 };
+					yield return new object [] { '<', true, true, true, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 226, 86 };
+					yield return new object [] { 'ç', false, false, false, 192, 39, (Key)'ç', 192, 39 };
+					yield return new object [] { 'Ç', true, false, false, 192, 39, (Key)'Ç' | Key.ShiftMask, 192, 39 };
+					yield return new object [] { 'ç', true, true, false, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask, 192, 39 };
+					yield return new object [] { 'ç', true, true, true, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 192, 39 };
+					yield return new object [] { '¨', false, true, true, 187, 26, (Key)'¨' | Key.AltMask | Key.CtrlMask, 187, 26 };
+					yield return new object [] { (uint)Key.PageUp, false, false, false, 33, 73, Key.PageUp, 33, 73 };
+					yield return new object [] { (uint)Key.PageUp, true, false, false, 33, 73, Key.PageUp | Key.ShiftMask, 33, 73 };
+					yield return new object [] { (uint)Key.PageUp, true, true, false, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask, 33, 73 };
+					yield return new object [] { (uint)Key.PageUp, true, true, true, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 33, 73 };
+				}
 			}
 
 			IEnumerator IEnumerable.GetEnumerator () => GetEnumerator ();

+ 1 - 0
UnitTests/Drivers/KeyTests.cs

@@ -1,4 +1,5 @@
 using System;
+using Terminal.Gui;
 using Xunit;
 
 namespace Terminal.Gui.DriverTests {

+ 42 - 5
UnitTests/TopLevels/ToplevelTests.cs

@@ -696,7 +696,7 @@ namespace Terminal.Gui.TopLevelTests {
 					((FakeDriver)Application.Driver).SetBufferSize (40, 15);
 					MessageBox.Query ("About", "Hello Word", "Ok");
 
-				} else if (iterations == 1) 					TestHelpers.AssertDriverContentsWithFrameAre (@"
+				} else if (iterations == 1) TestHelpers.AssertDriverContentsWithFrameAre (@"
  File                                   
 ┌ Window ──────────────────────────────┐
 │                                      │
@@ -712,7 +712,7 @@ namespace Terminal.Gui.TopLevelTests {
 │                                      │
 └──────────────────────────────────────┘
  CTRL-N New                             ", output);
-else if (iterations == 2) {
+				else if (iterations == 2) {
 					Assert.Null (Application.MouseGrabView);
 					// Grab the mouse
 					ReflectionTools.InvokePrivate (
@@ -815,8 +815,8 @@ else if (iterations == 2) {
 
 					Assert.Null (Application.MouseGrabView);
 
-				} else if (iterations == 8) 					Application.RequestStop ();
-else if (iterations == 9) 					Application.RequestStop ();
+				} else if (iterations == 8) Application.RequestStop ();
+				else if (iterations == 9) Application.RequestStop ();
 			};
 
 			Application.Run ();
@@ -956,7 +956,7 @@ else if (iterations == 9) 					Application.RequestStop ();
 
 					Assert.Null (Application.MouseGrabView);
 
-				} else if (iterations == 8) 					Application.RequestStop ();
+				} else if (iterations == 8) Application.RequestStop ();
 			};
 
 			Application.Run ();
@@ -974,5 +974,42 @@ else if (iterations == 9) 					Application.RequestStop ();
 			exception = Record.Exception (() => ((FakeDriver)Application.Driver).SetBufferSize (10, 0));
 			Assert.Null (exception);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void OnEnter_OnLeave_Triggered_On_Application_Begin_End ()
+		{
+			var isEnter = false;
+			var isLeave = false;
+			var v = new View ();
+			v.Enter += (_) => isEnter = true;
+			v.Leave += (_) => isLeave = true;
+			var top = Application.Top;
+			top.Add (v);
+
+			Assert.False (v.CanFocus);
+			var exception = Record.Exception (() => top.OnEnter (top));
+			Assert.Null (exception);
+			exception = Record.Exception (() => top.OnLeave (top));
+			Assert.Null (exception);
+
+			v.CanFocus = true;
+			Application.Begin (top);
+
+			Assert.True (isEnter);
+			Assert.False (isLeave);
+
+			isEnter = false;
+			var d = new Dialog ();
+			var rs = Application.Begin (d);
+
+			Assert.False (isEnter);
+			Assert.True (isLeave);
+
+			isLeave = false;
+			Application.End (rs);
+
+			Assert.True (isEnter);
+			Assert.False (isLeave);
+		}
 	}
 }

+ 86 - 0
UnitTests/Views/AutocompleteTests.cs

@@ -6,9 +6,16 @@ using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 using Terminal.Gui;
 using Xunit;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.ViewTests {
 	public class AutocompleteTests {
+		readonly ITestOutputHelper output;
+
+		public AutocompleteTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		[Fact]
 		public void Test_GenerateSuggestions_Simple ()
@@ -151,5 +158,84 @@ namespace Terminal.Gui.ViewTests {
 			Assert.Empty (tv.Autocomplete.Suggestions);
 			Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void CursorLeft_CursorRight_Mouse_Button_Pressed_Does_Not_Show_Popup ()
+		{
+			var tv = new TextView () {
+				Width = 50,
+				Height = 5,
+				Text = "This a long line and against TextView."
+			};
+			tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+")
+					.Select (s => s.Value)
+					.Distinct ().ToList ();
+			var top = Application.Top;
+			top.Add (tv);
+			Application.Begin (top);
+
+
+			for (int i = 0; i < 7; i++) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+				Application.Refresh ();
+				TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.", output);
+			}
+
+			Assert.True (tv.MouseEvent (new MouseEvent () {
+				X = 6,
+				Y = 0,
+				Flags = MouseFlags.Button1Pressed
+			}));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.g, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.
+       against                         ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.
+      against                          ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.
+     against                           ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.", output);
+
+			for (int i = 0; i < 3; i++) {
+				Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+				Application.Refresh ();
+				TestHelpers.AssertDriverContentsWithFrameAre (@"
+This ag long line and against TextView.", output);
+			}
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This a long line and against TextView.", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.n, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This an long line and against TextView.
+       and                             ", output);
+
+			Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+This an long line and against TextView.", output);
+		}
 	}
 }

+ 2 - 2
UnitTests/Views/TabViewTests.cs

@@ -719,7 +719,7 @@ namespace Terminal.Gui.ViewTests {
 
 			TestHelpers.AssertDriverContentsWithFrameAre (@"
 ┌──────────────┐    
-│Les Misrables│    
+│Les Misérables│    
 ◄              └───┐
 │hi2               │
 └──────────────────┘", output);
@@ -756,7 +756,7 @@ namespace Terminal.Gui.ViewTests {
 ┌──────────────────┐
 │hi2               │
 ◄              ┌───┘
-│Les Misrables│    
+│Les Misérables│    
 └──────────────┘    ", output);
 		}
 

+ 118 - 1
UnitTests/Views/TextFieldTests.cs

@@ -1,9 +1,18 @@
-using System;
+using NStack;
+using System;
+using System.Globalization;
 using System.Reflection;
 using Xunit;
+using Xunit.Abstractions;
 
 namespace Terminal.Gui.ViewTests {
 	public class TextFieldTests {
+		readonly ITestOutputHelper output;
+
+		public TextFieldTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
 
 		// This class enables test functions annotated with the [InitShutdown] attribute
 		// to have a function called before the test function is called and after.
@@ -1177,6 +1186,21 @@ namespace Terminal.Gui.ViewTests {
 			Assert.Equal ("-", newText);
 			Assert.Equal ("-1", oldText);
 			Assert.Equal ("-", tf.Text.ToString ());
+
+			// Delete word with accented char
+			tf.Text = "Les Misérables movie.";
+			Assert.True (tf.MouseEvent (new MouseEvent {
+				X = 7,
+				Y = 1,
+				Flags = MouseFlags.Button1DoubleClicked,
+				View = tf
+			}));
+			Assert.Equal ("Misérables ", tf.SelectedText);
+			Assert.Equal (11, tf.SelectedLength);
+			Assert.True (tf.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers ())));
+			Assert.Equal ("Les movie.", newText);
+			Assert.Equal ("Les Misérables movie.", oldText);
+			Assert.Equal ("Les movie.", tf.Text.ToString ());
 		}
 
 		[Fact]
@@ -1323,5 +1347,98 @@ namespace Terminal.Gui.ViewTests {
 			Assert.Equal (0, tf.SelectedLength);
 			Assert.Null (tf.SelectedText);
 		}
+
+
+		[Fact]
+		public void WordBackward_WordForward_SelectedText_With_Accent ()
+		{
+			string text = "Les Misérables movie.";
+			var tf = new TextField (text) { Width = 30 };
+
+			Assert.Equal (21, text.Length);
+			Assert.Equal (21, tf.Text.RuneCount);
+			Assert.Equal (21, tf.Text.ConsoleWidth);
+
+			var runes = tf.Text.ToRuneList ();
+			Assert.Equal (21, runes.Count);
+			Assert.Equal (22, tf.Text.Length);
+
+			for (int i = 0; i < runes.Count; i++) {
+				var cs = text [i];
+				var cus = (char)runes [i];
+				Assert.Equal (cs, cus);
+			}
+
+			var idx = 15;
+			Assert.Equal ('m', text [idx]);
+			Assert.Equal ('m', (char)runes [idx]);
+			Assert.Equal ("m", ustring.Make (runes [idx]));
+
+			Assert.True (tf.MouseEvent (new MouseEvent {
+				X = idx,
+				Y = 1,
+				Flags = MouseFlags.Button1DoubleClicked,
+				View = tf
+			}));
+			Assert.Equal ("movie.", tf.SelectedText);
+
+			Assert.True (tf.MouseEvent (new MouseEvent {
+				X = idx + 1,
+				Y = 1,
+				Flags = MouseFlags.Button1DoubleClicked,
+				View = tf
+			}));
+			Assert.Equal ("movie.", tf.SelectedText);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Words_With_Accents_Incorrect_Order_Will_Result_With_Wrong_Accent_Place ()
+		{
+			var tf = new TextField ("Les Misérables") { Width = 30 };
+			var top = Application.Top;
+			top.Add (tf);
+			Application.Begin (top);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+Les Misérables", output);
+
+			tf.Text = "Les Mise" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "rables";
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+Les Misérables", output);
+
+			// incorrect order will result with a wrong accent place
+			tf.Text = "Les Mis" + char.ConvertFromUtf32 (int.Parse ("0301", NumberStyles.HexNumber)) + "erables";
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+Les Miśerables", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Accented_Letter_With_Three_Combining_Unicode_Chars ()
+		{
+			var tf = new TextField ("ắ") { Width = 3 };
+			var top = Application.Top;
+			top.Add (tf);
+			Application.Begin (top);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+ắ", output);
+
+			tf.Text = "\u1eaf";
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+ắ", output);
+
+			tf.Text = "\u0103\u0301";
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+ắ", output);
+
+			tf.Text = "\u0061\u0306\u0301";
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+ắ", output);
+		}
 	}
 }

+ 316 - 3
UnitTests/Views/ViewTests.cs

@@ -1,4 +1,5 @@
-using System;
+using NStack;
+using System;
 using Xunit;
 using Xunit.Abstractions;
 //using GraphViewTests = Terminal.Gui.Views.GraphViewTests;
@@ -1601,8 +1602,8 @@ Y
 					// Calling the Text constructor.
 					lbl = new Label (text);
 				}
-				lbl.ColorScheme = new ColorScheme ();
-				lbl.Redraw (lbl.Bounds);
+				Application.Top.Add (lbl);
+				Application.Top.Redraw (Application.Top.Bounds);
 
 				// should have the initial text
 				Assert.Equal ('t', driver.Contents [0, 0, 0]);
@@ -3991,6 +3992,7 @@ This is a tes
 			public bool IsKeyDown { get; set; }
 			public bool IsKeyPress { get; set; }
 			public bool IsKeyUp { get; set; }
+			public override ustring Text { get; set; }
 
 			public override bool OnKeyDown (KeyEvent keyEvent)
 			{
@@ -4009,6 +4011,41 @@ This is a tes
 				IsKeyUp = true;
 				return true;
 			}
+
+			public void CorrectRedraw (Rect bounds)
+			{
+				// Clear the old and new frame area
+				Clear (NeedDisplay);
+				DrawText ();
+			}
+
+			public void IncorrectRedraw (Rect bounds)
+			{
+				// Clear only the new frame area
+				Clear ();
+				DrawText ();
+			}
+
+			private void DrawText ()
+			{
+				var idx = 0;
+				for (int r = 0; r < Frame.Height; r++) {
+					for (int c = 0; c < Frame.Width; c++) {
+						if (idx < Text.Length) {
+							var rune = Text [idx];
+							if (rune != '\n') {
+								AddRune (c, r, Text [idx]);
+							}
+							idx++;
+							if (rune == '\n') {
+								break;
+							}
+						}
+					}
+				}
+				ClearLayoutNeeded ();
+				ClearNeedsDisplay ();
+			}
 		}
 
 		[Theory, AutoInitShutdown]
@@ -4176,5 +4213,281 @@ cccccccccccccccccccc", output);
 111111111111111111110", attributes);
 			}
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Frame ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.Frame = new Rect (1, 1, 10, 1);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (1, 1, 31, 3), view.NeedDisplay);
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0     
+ A text wit", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Pos_Dim ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.X = 1;
+			view.Y = 1;
+			view.Width = 10;
+			view.Height = 1;
+			Assert.Equal (new Rect (1, 1, 10, 1), view.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (1, 1, 31, 3), view.NeedDisplay);
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0     
+ A text wit", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Frame ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.Frame = new Rect (1, 1, 10, 1);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (1, 1, 31, 3), view.NeedDisplay);
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+ A text wit                  
+  A text with some long width
+   and also with two lines.  ", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Pos_Dim ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.X = 1;
+			view.Y = 1;
+			view.Width = 10;
+			view.Height = 1;
+			Assert.Equal (new Rect (1, 1, 10, 1), view.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (1, 1, 31, 3), view.NeedDisplay);
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+ A text wit                  
+  A text with some long width
+   and also with two lines.  ", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Frame ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.Frame = new Rect (3, 3, 10, 1);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (2, 2, 30, 2), view.NeedDisplay);
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0       
+             
+             
+   A text wit", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Pos_Dim ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.X = 3;
+			view.Y = 3;
+			view.Width = 10;
+			view.Height = 1;
+			Assert.Equal (new Rect (3, 3, 10, 1), view.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (2, 2, 30, 2), view.NeedDisplay);
+			view.CorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0       
+             
+             
+   A text wit", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Frame ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.Frame = new Rect (3, 3, 10, 1);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (2, 2, 30, 2), view.NeedDisplay);
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   A text witith two lines.  ", output);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Pos_Dim ()
+		{
+			var label = new Label ("At 0,0");
+			var view = new DerivedView () {
+				X = 2,
+				Y = 2,
+				Width = 30,
+				Height = 2,
+				Text = "A text with some long width\n and also with two lines."
+			};
+			var top = Application.Top;
+			top.Add (label, view);
+			Application.Begin (top);
+
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   and also with two lines.  ", output);
+
+			view.X = 3;
+			view.Y = 3;
+			view.Width = 10;
+			view.Height = 1;
+			Assert.Equal (new Rect (3, 3, 10, 1), view.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 1), view.Bounds);
+			Assert.Equal (new Rect (2, 2, 30, 2), view.NeedDisplay);
+			view.IncorrectRedraw (view.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0                       
+                             
+  A text with some long width
+   A text witith two lines.  ", output);
+		}
 	}
 }