Bläddra i källkod

Fixes #2371. V2 needs be merged with develop and be fixed from errors. (#2372)

* Illustrates #2331 (Scrollview not respecting clip) does not reproduce (#2332)

* Proves that the issue #2331 don't have reason to happen.

* fixes #2336

* Fixes #2331. ScrollView may not be honoring clip region; CustomButton shows outside

* More appropriate solution for the issue #2331.

* Start refactoring LineCanvas for mixing line style support (e.g. double into single)

* Add remaining resolvers

* Implement corner border style mixing in LineCanvas

* Refactor and simplify resolvers

* Move tests to Core folder and namespace to Terminal.Gui.CoreTests

* Fixes #2333. TextField is selecting badly a word on double click.

* Add unit test deleting a word with accented char.

* Fixes 2331. ScrollView may not be honoring clip region.

* Add a custom button scenario.

* Fixes #2350. Clipping broke (see Clipping scenario).

* Is preferable use NeedDisplay instead of Bounds.

---------

Co-authored-by: Tig Kindel <[email protected]>
Co-authored-by: tznind <[email protected]>

* Fixes ASCIICustomButton scenario.

* Adds Snake Scenario (#2353)

* Add empty snake scenario

* Move snake head around

* Snake now has a tail

* Rest of logic implementation

* Ctrl K D layout fixes

* Game gets faster as you collect more apples

* Adjust speed increase rate down

* Use white on black for snake and border and red for apple

* Fix ScenarioTests not Disposing Scenario

* Add disposes and fix to use LineCanvas.GenerateImage

* Fix stack overflow, doh!

---------

Co-authored-by: Tig <[email protected]>

* Fixes 2368. Nested views with height of 1 not rendering correctly.

---------

Co-authored-by: Tig Kindel <[email protected]>
Co-authored-by: tznind <[email protected]>
Co-authored-by: Thomas Nind <[email protected]>
BDisp 2 år sedan
förälder
incheckning
cacfe0d772

+ 3 - 1
Terminal.Gui/Core/TextFormatter.cs

@@ -1190,7 +1190,9 @@ namespace Terminal.Gui {
 			for (int line = 0; line < linesFormated.Count; line++) {
 				if ((isVertical && line > bounds.Width) || (!isVertical && line > bounds.Height))
 					continue;
-				if ((isVertical && line > maxBounds.Left + maxBounds.Width - bounds.X) || (!isVertical && line > maxBounds.Top + maxBounds.Height - bounds.Y))
+				if ((isVertical && line >= maxBounds.Left + maxBounds.Width)
+					|| (!isVertical && line >= maxBounds.Top + maxBounds.Height))
+
 					break;
 
 				var runes = lines [line].ToRunes ();

+ 15 - 11
Terminal.Gui/Core/View.cs

@@ -1105,15 +1105,8 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public void Clear ()
 		{
-			Rect containerBounds = GetContainerBounds ();
-			Rect viewBounds = Bounds;
-			if (!containerBounds.IsEmpty) {
-				viewBounds.Width = Math.Min (viewBounds.Width, containerBounds.Width);
-				viewBounds.Height = Math.Min (viewBounds.Height, containerBounds.Height);
-			}
-
-			var h = viewBounds.Height;
-			var w = viewBounds.Width;
+			var h = Frame.Height;
+			var w = Frame.Width;
 			for (var line = 0; line < h; line++) {
 				Move (0, line);
 				for (var col = 0; col < w; col++)
@@ -1527,13 +1520,13 @@ namespace Terminal.Gui {
 			}
 
 			if (!ustring.IsNullOrEmpty (TextFormatter.Text)) {
-				Clear ();
+				Rect containerBounds = GetContainerBounds ();
+				Clear (ViewToScreen (GetNeedDisplay (containerBounds)));
 				SetChildNeedsDisplay ();
 				// Draw any Text
 				if (TextFormatter != null) {
 					TextFormatter.NeedsFormat = true;
 				}
-				Rect containerBounds = GetContainerBounds ();
 				TextFormatter?.Draw (ViewToScreen (boundsAdjustedForBorder), HasFocus ? ColorScheme.Focus : GetNormalColor (),
 				    HasFocus ? ColorScheme.HotFocus : Enabled ? ColorScheme.HotNormal : ColorScheme.Disabled,
 				    containerBounds);
@@ -1571,6 +1564,17 @@ namespace Terminal.Gui {
 			ClearNeedsDisplay ();
 		}
 
+		Rect GetNeedDisplay (Rect containerBounds)
+		{
+			Rect rect = NeedDisplay;
+			if (!containerBounds.IsEmpty) {
+				rect.Width = Math.Min (NeedDisplay.Width, containerBounds.Width);
+				rect.Height = Math.Min (NeedDisplay.Height, containerBounds.Height);
+			}
+
+			return rect;
+		}
+
 		Rect GetContainerBounds ()
 		{
 			var containerBounds = SuperView == null ? default : SuperView.ViewToScreen (SuperView.Bounds);

+ 313 - 0
UICatalog/Scenarios/ASCIICustomButton.cs

@@ -0,0 +1,313 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "ASCIICustomButtonTest", Description: "ASCIICustomButton sample")]
+	[ScenarioCategory ("Controls")]
+	public class ASCIICustomButtonTest : Scenario {
+		private static bool smallerWindow;
+		private ScrollViewTestWindow scrollViewTestWindow;
+		private MenuItem miSmallerWindow;
+
+		public override void Init (ColorScheme colorScheme)
+		{
+			Application.Init ();
+			scrollViewTestWindow = new ScrollViewTestWindow ();
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem("Window Size", new MenuItem [] {
+					miSmallerWindow = new MenuItem ("Smaller Window", "", ChangeWindowSize) {
+						CheckType = MenuItemCheckStyle.Checked
+					},
+					null,
+					new MenuItem("Quit", "",() => Application.RequestStop(),null,null, Key.Q | Key.CtrlMask)
+				})
+			});
+			Application.Top.Add (menu, scrollViewTestWindow);
+			Application.Run ();
+		}
+
+		private void ChangeWindowSize ()
+		{
+			smallerWindow = (bool)(miSmallerWindow.Checked = !miSmallerWindow.Checked);
+			scrollViewTestWindow.Dispose ();
+			Application.Top.Remove (scrollViewTestWindow);
+			scrollViewTestWindow = new ScrollViewTestWindow ();
+			Application.Top.Add (scrollViewTestWindow);
+		}
+
+		public override void Run ()
+		{
+		}
+
+		public class ASCIICustomButton : Button {
+			public string Description => $"Description of: {id}";
+
+			public event Action<ASCIICustomButton> PointerEnter;
+
+			private Label fill;
+			private FrameView border;
+			private string id;
+
+			public ASCIICustomButton (string text, Pos x, Pos y, int width, int height) : base (text)
+			{
+				CustomInitialize ("", text, x, y, width, height);
+			}
+
+			public ASCIICustomButton (string id, string text, Pos x, Pos y, int width, int height) : base (text)
+			{
+				CustomInitialize (id, text, x, y, width, height);
+			}
+
+			private void CustomInitialize (string id, string text, Pos x, Pos y, int width, int height)
+			{
+				this.id = id;
+				X = x;
+				Y = y;
+
+				Frame = new Rect {
+					Width = width,
+					Height = height
+				};
+
+				border = new FrameView () {
+					Width = width,
+					Height = height
+				};
+
+				AutoSize = false;
+
+				var fillText = new System.Text.StringBuilder ();
+				for (int i = 0; i < Bounds.Height; i++) {
+					if (i > 0) {
+						fillText.AppendLine ("");
+					}
+					for (int j = 0; j < Bounds.Width; j++) {
+						fillText.Append ("█");
+					}
+				}
+
+				fill = new Label (fillText.ToString ()) {
+					Visible = false,
+					CanFocus = false
+				};
+
+				var title = new Label (text) {
+					X = Pos.Center (),
+					Y = Pos.Center (),
+				};
+
+				border.MouseClick += This_MouseClick;
+				border.Subviews [0].MouseClick += This_MouseClick;
+				fill.MouseClick += This_MouseClick;
+				title.MouseClick += This_MouseClick;
+
+				Add (border, fill, title);
+			}
+
+			private void This_MouseClick (MouseEventArgs obj)
+			{
+				OnMouseEvent (obj.MouseEvent);
+			}
+
+			public override bool OnMouseEvent (MouseEvent mouseEvent)
+			{
+				Debug.WriteLine ($"{mouseEvent.Flags}");
+				if (mouseEvent.Flags == MouseFlags.Button1Clicked) {
+					if (!HasFocus && SuperView != null) {
+						if (!SuperView.HasFocus) {
+							SuperView.SetFocus ();
+						}
+						SetFocus ();
+						SetNeedsDisplay ();
+					}
+
+					OnClicked ();
+					return true;
+				}
+				return base.OnMouseEvent (mouseEvent);
+			}
+
+			public override bool OnEnter (View view)
+			{
+				border.Visible = false;
+				fill.Visible = true;
+				PointerEnter.Invoke (this);
+				view = this;
+				return base.OnEnter (view);
+			}
+
+			public override bool OnLeave (View view)
+			{
+				border.Visible = true;
+				fill.Visible = false;
+				if (view == null)
+					view = this;
+				return base.OnLeave (view);
+			}
+		}
+
+		public class ScrollViewTestWindow : Window {
+			private List<Button> buttons;
+			private const int BUTTONS_ON_PAGE = 7;
+			private const int BUTTON_HEIGHT = 3;
+
+			private ScrollView scrollView;
+			private ASCIICustomButton selected;
+
+			public ScrollViewTestWindow ()
+			{
+				Title = "ScrollViewTestWindow";
+
+				Label titleLabel = null;
+				if (smallerWindow) {
+					Width = 80;
+					Height = 25;
+
+					scrollView = new ScrollView () {
+						X = 3,
+						Y = 1,
+						Width = 24,
+						Height = BUTTONS_ON_PAGE * BUTTON_HEIGHT,
+						ShowVerticalScrollIndicator = true,
+						ShowHorizontalScrollIndicator = false
+					};
+				} else {
+					Width = Dim.Fill ();
+					Height = Dim.Fill ();
+
+					titleLabel = new Label ("DOCUMENTS") {
+						X = 0,
+						Y = 0
+					};
+
+					scrollView = new ScrollView () {
+						X = 0,
+						Y = 1,
+						Width = 27,
+						Height = BUTTONS_ON_PAGE * BUTTON_HEIGHT,
+						ShowVerticalScrollIndicator = true,
+						ShowHorizontalScrollIndicator = false
+					};
+				}
+
+				scrollView.ClearKeybindings ();
+
+				buttons = new List<Button> ();
+				Button prevButton = null;
+				int count = 20;
+				for (int j = 0; j < count; j++) {
+					Pos yPos = prevButton == null ? 0 : Pos.Bottom (prevButton);
+					var button = new ASCIICustomButton (j.ToString (), $"section {j}", 0, yPos, 25, BUTTON_HEIGHT);
+					button.Id = $"button{j}";
+					button.Clicked += Button_Clicked;
+					button.PointerEnter += Button_PointerEnter;
+					button.MouseClick += Button_MouseClick;
+					button.KeyPress += Button_KeyPress;
+					scrollView.Add (button);
+					buttons.Add (button);
+					prevButton = button;
+				}
+
+				var closeButton = new ASCIICustomButton ("close", "Close", 0, Pos.Bottom (prevButton), 25, BUTTON_HEIGHT);
+				closeButton.Clicked += Button_Clicked;
+				closeButton.PointerEnter += Button_PointerEnter;
+				closeButton.MouseClick += Button_MouseClick;
+				closeButton.KeyPress += Button_KeyPress;
+				scrollView.Add (closeButton);
+				buttons.Add (closeButton);
+
+				var pages = buttons.Count / BUTTONS_ON_PAGE;
+				if (buttons.Count % BUTTONS_ON_PAGE > 0)
+					pages++;
+
+				scrollView.ContentSize = new Size (25, pages * BUTTONS_ON_PAGE * BUTTON_HEIGHT);
+				if (smallerWindow) {
+					Add (scrollView);
+				} else {
+					Add (titleLabel, scrollView);
+				}
+			}
+
+			private void Button_KeyPress (KeyEventEventArgs obj)
+			{
+				switch (obj.KeyEvent.Key) {
+				case Key.End:
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						 -(scrollView.ContentSize.Height - scrollView.Frame.Height
+						 + (scrollView.ShowHorizontalScrollIndicator ? 1 : 0)));
+					obj.Handled = true;
+					return;
+				case Key.Home:
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X, 0);
+					obj.Handled = true;
+					return;
+				case Key.PageDown:
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						 Math.Max (scrollView.ContentOffset.Y - scrollView.Frame.Height,
+						 -(scrollView.ContentSize.Height - scrollView.Frame.Height
+						 + (scrollView.ShowHorizontalScrollIndicator ? 1 : 0))));
+					obj.Handled = true;
+					return;
+				case Key.PageUp:
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						 Math.Min (scrollView.ContentOffset.Y + scrollView.Frame.Height, 0));
+					obj.Handled = true;
+					return;
+				}
+			}
+
+			private void Button_MouseClick (MouseEventArgs obj)
+			{
+				if (obj.MouseEvent.Flags == MouseFlags.WheeledDown) {
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						scrollView.ContentOffset.Y - BUTTON_HEIGHT);
+					obj.Handled = true;
+				} else if (obj.MouseEvent.Flags == MouseFlags.WheeledUp) {
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						Math.Min (scrollView.ContentOffset.Y + BUTTON_HEIGHT, 0));
+					obj.Handled = true;
+				}
+			}
+
+			private void Button_Clicked ()
+			{
+				MessageBox.Query ("Button clicked.", $"'{selected.Text}' clicked!", "Ok");
+				if (selected.Text == "Close") {
+					Application.RequestStop ();
+				}
+			}
+
+			private void Button_PointerEnter (ASCIICustomButton obj)
+			{
+				bool? moveDown;
+				if (obj.Frame.Y > selected?.Frame.Y) {
+					moveDown = true;
+				} else if (obj.Frame.Y < selected?.Frame.Y) {
+					moveDown = false;
+				} else {
+					moveDown = null;
+				}
+				var offSet = selected != null ? obj.Frame.Y - selected.Frame.Y + (-scrollView.ContentOffset.Y % BUTTON_HEIGHT) : 0;
+				selected = obj;
+				if (moveDown == true && selected.Frame.Y + scrollView.ContentOffset.Y + BUTTON_HEIGHT >= scrollView.Frame.Height && offSet != BUTTON_HEIGHT) {
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						Math.Min (scrollView.ContentOffset.Y - BUTTON_HEIGHT, -(selected.Frame.Y - scrollView.Frame.Height + BUTTON_HEIGHT)));
+				} else if (moveDown == true && selected.Frame.Y + scrollView.ContentOffset.Y >= scrollView.Frame.Height) {
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						scrollView.ContentOffset.Y - BUTTON_HEIGHT);
+				} else if (moveDown == true && selected.Frame.Y + scrollView.ContentOffset.Y < 0) {
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						-selected.Frame.Y);
+				} else if (moveDown == false && selected.Frame.Y < -scrollView.ContentOffset.Y) {
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						Math.Max (scrollView.ContentOffset.Y + BUTTON_HEIGHT, selected.Frame.Y));
+				} else if (moveDown == false && selected.Frame.Y + scrollView.ContentOffset.Y > scrollView.Frame.Height) {
+					scrollView.ContentOffset = new Point (scrollView.ContentOffset.X,
+						 -(selected.Frame.Y - scrollView.Frame.Height + BUTTON_HEIGHT));
+				}
+			}
+		}
+	}
+}

+ 1 - 1
UICatalog/Scenarios/Animation.cs

@@ -71,7 +71,7 @@ namespace UICatalog.Scenarios {
 		protected override void Dispose(bool disposing)
 		{
 			isDisposed = true;
-			base.Dispose();
+			base.Dispose(disposing);
 		}
 
 		// This is a C# port of https://github.com/andraaspar/bitmap-to-braille by Andraaspar

+ 353 - 0
UICatalog/Scenarios/Snake.cs

@@ -0,0 +1,353 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using Terminal.Gui;
+using Terminal.Gui.Graphs;
+using Attribute = Terminal.Gui.Attribute;
+
+namespace UICatalog.Scenarios {
+	[ScenarioMetadata (Name: "Snake", Description: "The game of apple eating.")]
+	[ScenarioCategory ("Colors")]
+	public class Snake : Scenario {
+		private bool isDisposed;
+
+		public override void Setup ()
+		{
+			base.Setup ();
+
+			var state = new SnakeState ();
+
+			state.Reset (60, 20);
+
+			var snakeView = new SnakeView (state) {
+				Width = state.Width,
+				Height = state.Height
+			};
+
+
+			Win.Add (snakeView);
+
+			Stopwatch sw = new Stopwatch ();
+
+			Task.Run (() => {
+				while (!isDisposed) {
+
+					sw.Restart ();
+
+					if (state.AdvanceState ()) {
+
+						// When updating from a Thread/Task always use Invoke
+						Application.MainLoop?.Invoke (() => {
+							snakeView.SetNeedsDisplay ();
+						});
+					}
+
+					var wait = state.SleepAfterAdvancingState - sw.ElapsedMilliseconds;
+
+					if (wait > 0) {
+						Task.Delay ((int)wait).Wait ();
+					}
+				}
+			});
+		}
+
+		protected override void Dispose (bool disposing)
+		{
+			isDisposed = true;
+			base.Dispose (disposing);
+		}
+
+		private class SnakeView : View {
+
+			private Attribute red = new Terminal.Gui.Attribute (Color.Red,Color.Black);
+			private Attribute white = new Terminal.Gui.Attribute (Color.White, Color.Black);
+
+			public SnakeState State { get; }
+
+			public SnakeView (SnakeState state)
+			{
+				State = state;
+				CanFocus = true;
+
+				ColorScheme = new ColorScheme {
+					Normal = white,
+					Focus = white,
+					HotNormal = white,
+					HotFocus = white,
+					Disabled = white
+				};
+			}
+
+			public override void Redraw (Rect bounds)
+			{
+				base.Redraw (bounds);
+
+				Driver.SetAttribute (white);
+				Clear ();
+
+				var canvas = new LineCanvas ();
+
+				canvas.AddLine (new Point (0, 0), State.Width - 1, Orientation.Horizontal, BorderStyle.Double);
+				canvas.AddLine (new Point (0, 0), State.Height - 1, Orientation.Vertical, BorderStyle.Double);
+				canvas.AddLine (new Point (0, State.Height - 1), State.Width - 1, Orientation.Horizontal, BorderStyle.Double);
+				canvas.AddLine (new Point (State.Width - 1, 0), State.Height - 1, Orientation.Vertical, BorderStyle.Double);
+
+				for (int i = 1; i < State.Snake.Count; i++) {
+
+					var pt1 = State.Snake [i - 1];
+					var pt2 = State.Snake [i];
+
+					var orientation = pt1.X == pt2.X ? Orientation.Vertical : Orientation.Horizontal;
+					var length = orientation == Orientation.Horizontal
+						? pt1.X > pt2.X ? 1 : -1
+						: pt1.Y > pt2.Y ? 1 : -1;
+
+					canvas.AddLine (
+						pt2,
+						length,
+						orientation,
+						BorderStyle.Single);
+
+				}
+
+				foreach(var p in canvas.GenerateImage (bounds)) {
+					AddRune (p.Key.X, p.Key.Y, p.Value);
+				}
+
+
+				Driver.SetAttribute (red);
+				AddRune (State.Apple.X, State.Apple.Y, 'A');
+				Driver.SetAttribute (white);
+			}
+			public override bool OnKeyDown (KeyEvent keyEvent)
+			{
+				if (keyEvent.Key == Key.CursorUp) {
+					State.PlannedDirection = Direction.Up;
+					return true;
+				}
+				if (keyEvent.Key == Key.CursorDown) {
+					State.PlannedDirection = Direction.Down;
+					return true;
+				}
+				if (keyEvent.Key == Key.CursorLeft) {
+					State.PlannedDirection = Direction.Left;
+					return true;
+				}
+				if (keyEvent.Key == Key.CursorRight) {
+					State.PlannedDirection = Direction.Right;
+					return true;
+				}
+
+				return false;
+			}
+		}
+		private class SnakeState {
+
+			public const int StartingLength = 10;
+			public const int AppleGrowRate = 5;
+			public const int StartingSpeed = 50;
+			public const int MaxSpeed = 20;
+
+			public int Width { get; private set; }
+			public int Height { get; private set; }
+
+			/// <summary>
+			/// Position of the snakes head
+			/// </summary>
+			public Point Head => Snake.Last ();
+
+			/// <summary>
+			/// Current position of the Apple that the snake has to eat.
+			/// </summary>
+			public Point Apple { get; private set; }
+
+			public Direction CurrentDirection { get; private set; }
+			public Direction PlannedDirection { get; set; }
+
+			public List<Point> Snake { get; private set; }
+
+			public int SleepAfterAdvancingState { get; private set; } = StartingSpeed;
+
+			int step;
+
+			internal bool AdvanceState ()
+			{
+				step++;
+
+				if (step < GetStepVelocity ()) {
+					return false;
+				}
+
+				step = 0;
+
+				UpdateDirection ();
+
+				var newHead = GetNewHeadPoint ();
+
+				Snake.RemoveAt (0);
+				Snake.Add (newHead);
+
+				if (IsDeath (newHead)) {
+					GameOver ();
+				}
+
+				if (newHead == Apple) {
+					GrowSnake (AppleGrowRate);
+					Apple = GetNewRandomApplePoint ();
+
+					var delta = 5;
+					if(SleepAfterAdvancingState < 40) {
+						delta = 3;
+					}
+					if (SleepAfterAdvancingState < 30) {
+						delta = 2;
+					}
+					SleepAfterAdvancingState = Math.Max (MaxSpeed, SleepAfterAdvancingState - delta);
+				}
+
+				return true;
+			}
+
+			private int GetStepVelocity ()
+			{
+				if (CurrentDirection == Direction.Left || CurrentDirection == Direction.Right) {
+					return 1;
+				}
+
+				return 2;
+			}
+
+			public void GrowSnake ()
+			{
+				var tail = Snake.First ();
+				Snake.Insert (0, tail);
+			}
+			public void GrowSnake (int amount)
+			{
+				for (int i = 0; i < amount; i++) {
+					GrowSnake ();
+				}
+			}
+
+			private void UpdateDirection ()
+			{
+				if (!AreOpposites (CurrentDirection, PlannedDirection)) {
+					CurrentDirection = PlannedDirection;
+				}
+			}
+
+			private bool AreOpposites (Direction a, Direction b)
+			{
+				switch (a) {
+				case Direction.Left: return b == Direction.Right;
+				case Direction.Right: return b == Direction.Left;
+				case Direction.Up: return b == Direction.Down;
+				case Direction.Down: return b == Direction.Up;
+				}
+
+				return false;
+			}
+
+			private void GameOver ()
+			{
+				Reset (Width, Height);
+			}
+
+			private Point GetNewHeadPoint ()
+			{
+				switch (CurrentDirection) {
+				case Direction.Left:
+					return new Point (Head.X - 1, Head.Y);
+
+				case Direction.Right:
+					return new Point (Head.X + 1, Head.Y);
+
+				case Direction.Up:
+					return new Point (Head.X, Head.Y - 1);
+
+				case Direction.Down:
+					return new Point (Head.X, Head.Y + 1);
+				}
+
+				throw new Exception ("Unknown direction");
+			}
+
+			/// <summary>
+			/// Restarts the game with the given canvas size
+			/// </summary>
+			/// <param name="width"></param>
+			/// <param name="height"></param>
+			internal void Reset (int width, int height)
+			{
+				if (width < 5 || height < 5) {
+					return;
+				}
+
+				Width = width;
+				Height = height;
+
+				var middle = new Point (width / 2, height / 2);
+
+				// Start snake with a length of 2
+				Snake = new List<Point> { middle, middle };
+				Apple = GetNewRandomApplePoint ();
+
+				SleepAfterAdvancingState = StartingSpeed;
+
+				GrowSnake (StartingLength);
+			}
+
+			private Point GetNewRandomApplePoint ()
+			{
+				Random r = new Random ();
+
+				for (int i = 0; i < 1000; i++) {
+					var x = r.Next (0, Width);
+					var y = r.Next (0, Height);
+
+					var p = new Point (x, y);
+
+					if (p == Head) {
+						continue;
+					}
+
+					if (IsDeath (p)) {
+						continue;
+					}
+
+					return p;
+				}
+
+				// Game is won or we are unable to generate a valid apple
+				// point after 1000 attempts.  Maybe screen size is very small
+				// or something.  Either way restart the game.
+				Reset (Width, Height);
+				return Apple;
+			}
+
+			private bool IsDeath (Point p)
+			{
+				if (p.X <= 0 || p.X >= Width - 1) {
+					return true;
+				}
+
+				if (p.Y <= 0 || p.Y >= Height - 1) {
+					return true;
+				}
+
+				if (Snake.Take (Snake.Count - 1).Contains (p))
+					return true;
+
+				return false;
+			}
+		}
+		private enum Direction {
+			Up,
+			Down,
+			Left,
+			Right
+		}
+	}
+}

+ 2 - 0
UICatalog/UICatalog.cs

@@ -87,6 +87,7 @@ namespace UICatalog {
 				_selectedScenario.Init (Colors.ColorSchemes [_topLevelColorScheme]);
 				_selectedScenario.Setup ();
 				_selectedScenario.Run ();
+				_selectedScenario.Dispose ();
 				_selectedScenario = null;
 				Application.Shutdown ();
 				return;
@@ -110,6 +111,7 @@ namespace UICatalog {
 				scenario.Init (Colors.ColorSchemes [_topLevelColorScheme]);
 				scenario.Setup ();
 				scenario.Run ();
+				scenario.Dispose ();
 
 				// This call to Application.Shutdown brackets the Application.Init call
 				// made by Scenario.Init() above

+ 65 - 12
UnitTests/Core/BorderTests.cs

@@ -1,15 +1,19 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.Reflection.Emit;
 using Xunit;
+using Xunit.Abstractions;
 using Rune = System.Rune;
 
 namespace Terminal.Gui.CoreTests {
 	public class BorderTests {
-		[Fact]
-		[AutoInitShutdown]
+		readonly ITestOutputHelper output;
+
+		public BorderTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		[Fact, AutoInitShutdown]
 		public void Constructor_Defaults ()
 		{
 			var b = new Border ();
@@ -45,8 +49,7 @@ namespace Terminal.Gui.CoreTests {
 			Assert.False (b.DrawMarginFrame);
 		}
 
-		[Fact]
-		[AutoInitShutdown]
+		[Fact, AutoInitShutdown]
 		public void ActualWidth_ActualHeight ()
 		{
 			var v = new View (new Rect (5, 10, 60, 20), "", new Border ());
@@ -303,8 +306,7 @@ namespace Terminal.Gui.CoreTests {
 		//	}
 		//}
 
-		[Fact]
-		[AutoInitShutdown]
+		[Fact, AutoInitShutdown]
 		public void DrawContent_With_Parent_Border ()
 		{
 			var top = Application.Top;
@@ -540,8 +542,7 @@ namespace Terminal.Gui.CoreTests {
 			}
 		}
 
-		[Fact]
-		[AutoInitShutdown]
+		[Fact, AutoInitShutdown]
 		public void BorderOnControlWithNoChildren ()
 		{
 			var label = new TextField ("Loading...") {
@@ -557,5 +558,57 @@ namespace Terminal.Gui.CoreTests {
 
 			Assert.Null (Record.Exception (() => label.Redraw (label.Bounds)));
 		}
+
+		[Fact, AutoInitShutdown]
+		public void BorderStyle_And_DrawMarginFrame_Gets_Sets ()
+		{
+			var lblTop = new Label ("At 0,0");
+			var lblFrame = new Label ("Centered") { X = Pos.Center (), Y = Pos.Center () };
+			var frame = new FrameView () { Y = 1, Width = 20, Height = 3 };
+			var lblFill = new Label () { Width = Dim.Fill(),Height = Dim.Fill(), Visible = false };
+			var fillText = new System.Text.StringBuilder ();
+			for (int i = 0; i < frame.Bounds.Height; i++) {
+				if (i > 0) {
+					fillText.AppendLine ("");
+				}
+				for (int j = 0; j < frame.Bounds.Width; j++) {
+					fillText.Append ("█");
+				}
+			}
+			lblFill.Text = fillText.ToString ();
+			frame.Add (lblFill, lblFrame);
+			var lblBottom = new Label ("At 0,4") { Y = 4 };
+			Application.Top.Add (lblTop, frame, lblBottom);
+			Application.Begin (Application.Top);
+
+			Assert.Equal (BorderStyle.Single, frame.Border.BorderStyle);
+			Assert.True (frame.Border.DrawMarginFrame);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0              
+┌──────────────────┐
+│     Centered     │
+└──────────────────┘
+At 0,4              ", output);
+
+			frame.Border.BorderStyle = BorderStyle.None;
+			Application.Refresh ();
+			Assert.True (frame.Border.DrawMarginFrame);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0        
+              
+      Centered
+              
+At 0,4        ", output);
+
+			frame.Border.DrawMarginFrame = false;
+			lblFill.Visible = true;
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+At 0,0              
+████████████████████
+██████Centered██████
+████████████████████
+At 0,4              ", output);
+		}
 	}
 }

+ 5 - 0
UnitTests/UICatalog/ScenarioTests.cs

@@ -69,6 +69,9 @@ namespace UICatalog.Tests {
 				scenario.Init (Colors.Base);
 				scenario.Setup ();
 				scenario.Run ();
+
+				scenario.Dispose();
+
 				Application.Shutdown ();
 #if DEBUG_IDISPOSABLE
 				foreach (var inst in Responder.Instances) {
@@ -137,6 +140,8 @@ namespace UICatalog.Tests {
 			// Using variable in the left side of Assert.Equal/NotEqual give error. Must be used literals values.
 			//Assert.Equal (stackSize, iterations);
 
+			generic.Dispose();
+
 			// Shutdown must be called to safely clean up Application if Init has been called
 			Application.Shutdown ();
 

+ 223 - 5
UnitTests/Views/ScrollViewTests.cs

@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using NStack;
 using Xunit;
 using Xunit.Abstractions;
 
@@ -280,5 +276,227 @@ namespace Terminal.Gui.ViewTests {
 ◄░░░├─┤░► 
 ", output);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Frame_And_Labels_Does_Not_Overspill_ScrollView ()
+		{
+			var sv = new ScrollView {
+				X = 3,
+				Y = 3,
+				Width = 10,
+				Height = 10,
+				ContentSize = new Size (50, 50)
+			};
+			for (int i = 0; i < 8; i++) {
+				sv.Add (new CustomButton ("█", $"Button {i}", 20, 3) { Y = i * 3 });
+			}
+			Application.Top.Add (sv);
+			Application.Begin (Application.Top);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+   █████████▲
+   ██████But┬
+   █████████┴
+   ┌────────░
+   │     But░
+   └────────░
+   ┌────────░
+   │     But░
+   └────────▼
+   ◄├┤░░░░░► ", output);
+
+			sv.ContentOffset = new Point (5, 5);
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+   ─────────▲
+   ─────────┬
+    Button 2│
+   ─────────┴
+   ─────────░
+    Button 3░
+   ─────────░
+   ─────────░
+    Button 4▼
+   ◄├─┤░░░░► ", output);
+		}
+
+		private class CustomButton : FrameView {
+			private Label labelFill;
+			private Label labelText;
+
+			public CustomButton (string fill, ustring text, int width, int height)
+			{
+				Width = width;
+				Height = height;
+				labelFill = new Label () { AutoSize = false, Width = Dim.Fill (), Height = Dim.Fill (), Visible = false };
+				var fillText = new System.Text.StringBuilder ();
+				for (int i = 0; i < Bounds.Height; i++) {
+					if (i > 0) {
+						fillText.AppendLine ("");
+					}
+					for (int j = 0; j < Bounds.Width; j++) {
+						fillText.Append (fill);
+					}
+				}
+				labelFill.Text = fillText.ToString ();
+				labelText = new Label (text) { X = Pos.Center (), Y = Pos.Center () };
+				Add (labelFill, labelText);
+				CanFocus = true;
+			}
+
+			public override bool OnEnter (View view)
+			{
+				Border.BorderStyle = BorderStyle.None;
+				Border.DrawMarginFrame = false;
+				labelFill.Visible = true;
+				view = this;
+				return base.OnEnter (view);
+			}
+
+			public override bool OnLeave (View view)
+			{
+				Border.BorderStyle = BorderStyle.Single;
+				Border.DrawMarginFrame = true;
+				labelFill.Visible = false;
+				if (view == null)
+					view = this;
+				return base.OnLeave (view);
+			}
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Clear_Window_Inside_ScrollView ()
+		{
+			var topLabel = new Label ("At 15,0") { X = 15 };
+			var sv = new ScrollView {
+				X = 3,
+				Y = 3,
+				Width = 10,
+				Height = 10,
+				ContentSize = new Size (23, 23),
+				KeepContentAlwaysInViewport = false
+			};
+			var bottomLabel = new Label ("At 15,15") { X = 15, Y = 15 };
+			Application.Top.Add (topLabel, sv, bottomLabel);
+			Application.Begin (Application.Top);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+               At 15,0 
+                       
+                       
+            ▲          
+            ┬          
+            ┴          
+            ░          
+            ░          
+            ░          
+            ░          
+            ░          
+            ▼          
+   ◄├┤░░░░░►           
+                       
+                       
+               At 15,15", output);
+
+			var attributes = new Attribute [] {
+				Colors.TopLevel.Normal,
+				Colors.TopLevel.Focus,
+				Colors.Base.Normal
+			};
+
+			TestHelpers.AssertDriverColorsAre (@"
+00000000000000000000000
+00000000000000000000000
+00000000000000000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00011111111110000000000
+00000000000000000000000
+00000000000000000000000
+00000000000000000000000", attributes);
+
+			sv.Add (new Window ("1") { X = 3, Y = 3, Width = 20, Height = 20 });
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+               At 15,0 
+                       
+                       
+            ▲          
+            ┬          
+            ┴          
+      ┌ 1 ──░          
+      │     ░          
+      │     ░          
+      │     ░          
+      │     ░          
+      │     ▼          
+   ◄├┤░░░░░►           
+                       
+                       
+               At 15,15", output);
+
+			TestHelpers.AssertDriverColorsAre (@"
+00000000000000000000000
+00000000000000000000000
+00000000000000000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000022222210000000000
+00000022222210000000000
+00000022222210000000000
+00000022222210000000000
+00000022222210000000000
+00000022222210000000000
+00011111111110000000000
+00000000000000000000000
+00000000000000000000000
+00000000000000000000000", attributes);
+
+			sv.ContentOffset = new Point (20, 20);
+			Application.Refresh ();
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+               At 15,0 
+                       
+                       
+     │      ▲          
+     │      ░          
+   ──┘      ░          
+            ░          
+            ░          
+            ┬          
+            │          
+            ┴          
+            ▼          
+   ◄░░░░├─┤►           
+                       
+                       
+               At 15,15", output);
+
+			TestHelpers.AssertDriverColorsAre (@"
+00000000000000000000000
+00000000000000000000000
+00000000000000000000000
+00022200000010000000000
+00022200000010000000000
+00022200000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00000000000010000000000
+00011111111110000000000
+00000000000000000000000
+00000000000000000000000
+00000000000000000000000", attributes);
+		}
 	}
 }

+ 27 - 0
UnitTests/Views/ViewTests.cs

@@ -1,5 +1,6 @@
 using NStack;
 using System;
+using Terminal.Gui.Graphs;
 using Xunit;
 using Xunit.Abstractions;
 //using GraphViewTests = Terminal.Gui.Views.GraphViewTests;
@@ -4489,5 +4490,31 @@ At 0,0
   A text with some long width
    A text witith two lines.  ", output);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Test_Nested_Views_With_Height_Equal_To_One ()
+		{
+			var v = new View () { Width = 11, Height = 3, ColorScheme = new ColorScheme () };
+
+			var top = new View () { Width = Dim.Fill (), Height = 1 };
+			var bottom = new View () { Width = Dim.Fill (), Height = 1, Y = 2 };
+
+			top.Add (new Label ("111"));
+			v.Add (top);
+			v.Add (new LineView (Orientation.Horizontal) { Y = 1 });
+			bottom.Add (new Label ("222"));
+			v.Add (bottom);
+
+			v.LayoutSubviews ();
+			v.Redraw (v.Bounds);
+
+
+			string looksLike =
+@"    
+111
+───────────
+222";
+			TestHelpers.AssertDriverContentsAre (looksLike, output);
+		}
 	}
 }