瀏覽代碼

Merge pull request #693 from tig/idisposable

Adds IDisposable to Responder and implements Dispose pattern throughout
Charlie Kindel 5 年之前
父節點
當前提交
24d382f7a8

+ 2 - 0
Terminal.Gui/Core/Application.cs

@@ -241,6 +241,7 @@ namespace Terminal.Gui {
 			{
 				if (Toplevel != null) {
 					End (Toplevel, disposing);
+					Toplevel.Dispose ();
 					Toplevel = null;
 				}
 			}
@@ -500,6 +501,7 @@ namespace Terminal.Gui {
 			// TODO: Some of this state is actually related to Begin/End (not Init/Shutdown) and should be moved to `RunState` (#520)
 			foreach (var t in toplevels) {
 				t.Running = false;
+				t.Dispose ();
 			}
 			toplevels.Clear ();
 			Current = null;

+ 67 - 1
Terminal.Gui/Core/Responder.cs

@@ -13,11 +13,39 @@
 // Optimziations
 //   - Add rendering limitation to the exposed area
 
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
 namespace Terminal.Gui {
 	/// <summary>
 	/// Responder base class implemented by objects that want to participate on keyboard and mouse input.
 	/// </summary>
-	public class Responder {
+	public class Responder : IDisposable {
+		bool disposedValue;
+
+#if DEBUG_IDISPOSABLE
+		/// <summary>
+		/// For debug purposes to verify objects are being disposed properly
+		/// </summary>
+		public bool WasDisposed = false;
+		/// <summary>
+		/// For debug purposes to verify objects are being disposed properly
+		/// </summary>
+		public int DisposedCount = 0;
+		/// <summary>
+		/// For debug purposes
+		/// </summary>
+		public static List<Responder> Instances = new List<Responder> ();
+		/// <summary>
+		/// For debug purposes
+		/// </summary>
+		public Responder ()
+		{
+			Instances.Add (this);
+		}
+#endif
+
 		/// <summary>
 		/// Gets or sets a value indicating whether this <see cref="Responder"/> can focus.
 		/// </summary>
@@ -182,5 +210,43 @@ namespace Terminal.Gui {
 		{
 			return false;
 		}
+
+		/// <summary>
+		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+		/// </summary>
+		/// <remarks>
+		/// If disposing equals true, the method has been called directly
+		/// or indirectly by a user's code. Managed and unmanaged resources
+		/// can be disposed.
+		/// If disposing equals false, the method has been called by the
+		/// runtime from inside the finalizer and you should not reference
+		/// other objects. Only unmanaged resources can be disposed.		
+		/// </remarks>
+		/// <param name="disposing"></param>
+		protected virtual void Dispose (bool disposing)
+		{
+			if (!disposedValue) {
+				if (disposing) {
+					// TODO: dispose managed state (managed objects)
+				}
+
+				// TODO: free unmanaged resources (unmanaged objects) and override finalizer
+				// TODO: set large fields to null
+				disposedValue = true;
+			}
+		}
+
+		/// <summary>
+		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource.
+		/// </summary>
+		public void Dispose ()
+		{
+			// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+			Dispose (disposing: true);
+			GC.SuppressFinalize (this);
+#if DEBUG_IDISPOSABLE
+			WasDisposed = true;
+#endif
+		}
 	}
 }

+ 50 - 30
Terminal.Gui/Core/TextFormatter.cs

@@ -35,7 +35,7 @@ namespace Terminal.Gui {
 		ustring text;
 		TextAlignment textAlignment;
 		Attribute textColor = -1;
-		bool needsFormat = true;
+		bool needsFormat;
 		Key hotKey;
 		Size size;
 
@@ -46,7 +46,14 @@ namespace Terminal.Gui {
 			get => text;
 			set {
 				text = value;
-				needsFormat = true;
+
+				if (Size.IsEmpty) {
+					// Proivde a default size (width = length of longest line, height = 1)
+					// TODO: It might makem more sense for the default to be width = length of first line?
+					Size = new Size (TextFormatter.MaxWidth (Text, int.MaxValue), 1);
+				}
+
+				NeedsFormat = true;
 			}
 		}
 
@@ -59,18 +66,18 @@ namespace Terminal.Gui {
 			get => textAlignment;
 			set {
 				textAlignment = value;
-				needsFormat = true;
+				NeedsFormat = true;
 			}
 		}
 
 		/// <summary>
-		///  Gets the size of the area the text will be drawn in. 
+		///  Gets or sets the size of the area the text will be constrainted to when formatted. 
 		/// </summary>
 		public Size Size {
 			get => size;
-			internal set {
+			set {
 				size = value;
-				needsFormat = true;
+				NeedsFormat = true;
 			}
 		}
 
@@ -96,40 +103,50 @@ namespace Terminal.Gui {
 		public uint HotKeyTagMask { get; set; } = 0x100000;
 
 		/// <summary>
-		/// Gets the formatted lines.
+		/// Gets the formatted lines. 
 		/// </summary>
+		/// <remarks>
+		/// <para>
+		/// Upon a 'get' of this property, if the text needs to be formatted (if <see cref="NeedsFormat"/> is <c>true</c>)
+		/// <see cref="Format(ustring, int, TextAlignment, bool)"/> will be called internally. 
+		/// </para>
+		/// </remarks>
 		public List<ustring> Lines {
 			get {
 				// With this check, we protect against subclasses with overrides of Text
 				if (ustring.IsNullOrEmpty (Text)) {
 					lines = new List<ustring> ();
 					lines.Add (ustring.Empty);
-					needsFormat = false;
+					NeedsFormat = false;
 					return lines;
 				}
 
-				if (needsFormat) {
+				if (NeedsFormat) {
 					var shown_text = text;
 					if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out hotKey)) {
 						shown_text = RemoveHotKeySpecifier (Text, hotKeyPos, HotKeySpecifier);
 						shown_text = ReplaceHotKeyWithTag (shown_text, hotKeyPos);
 					}
+					if (Size.IsEmpty) {
+						throw new InvalidOperationException ("Size must be set before accessing Lines");
+					}
 					lines = Format (shown_text, Size.Width, textAlignment, Size.Height > 1);
+					NeedsFormat = false;
 				}
-				needsFormat = false;
 				return lines;
 			}
 		}
 
 		/// <summary>
-		/// Sets a flag indicating the text needs to be formatted. 
-		/// Subsequent calls to <see cref="Draw"/>, <see cref="Lines"/>, etc... will cause the formatting to happen.>
+		/// Gets or sets whether the <see cref="TextFormatter"/> needs to format the text when <see cref="Draw(Rect, Attribute, Attribute)"/> is called. 
+		/// If it is <c>false</c> when Draw is called, the Draw call will be faster.
 		/// </summary>
-		public void SetNeedsFormat ()
-		{
-			needsFormat = true;
-		}
-
+		/// <remarks>
+		/// <para>
+		/// This is set to true when the properties of <see cref="TextFormatter"/> are set. 
+		/// </para>
+		/// </remarks>
+		public bool NeedsFormat { get => needsFormat; set => needsFormat = value; }
 
 		static ustring StripCRLF (ustring str)
 		{
@@ -199,14 +216,14 @@ namespace Terminal.Gui {
 				return lines;
 			}
 
-			var runes = StripCRLF (text).ToRuneList();
+			var runes = StripCRLF (text).ToRuneList ();
 
 			while ((end = start + width) < runes.Count) {
 				while (runes [end] != ' ' && end > start)
 					end -= 1;
 				if (end == start)
 					end = start + width;
-				lines.Add (ustring.Make (runes.GetRange (start, end - start)).TrimSpace());
+				lines.Add (ustring.Make (runes.GetRange (start, end - start)).TrimSpace ());
 				start = end;
 			}
 
@@ -236,7 +253,7 @@ namespace Terminal.Gui {
 			var runes = text.ToRuneList ();
 			int slen = runes.Count;
 			if (slen > width) {
-				return ustring.Make (runes.GetRange(0, width));
+				return ustring.Make (runes.GetRange (0, width));
 			} else {
 				if (talign == TextAlignment.Justified) {
 					return Justify (text, width);
@@ -303,6 +320,9 @@ namespace Terminal.Gui {
 		/// <para>
 		/// If <c>width</c> is 0, a single, empty line will be returned.
 		/// </para>
+		/// <para>
+		/// If <c>width</c> is int.MaxValue, the text will be formatted to the maximum width possible. 
+		/// </para>
 		/// </remarks>
 		public static List<ustring> Format (ustring text, int width, TextAlignment talign, bool wordWrap)
 		{
@@ -329,7 +349,7 @@ namespace Terminal.Gui {
 			for (int i = 0; i < runeCount; i++) {
 				Rune c = text [i];
 				if (c == '\n') {
-					var wrappedLines = WordWrap (ustring.Make (runes.GetRange(lp, i - lp)), width);
+					var wrappedLines = WordWrap (ustring.Make (runes.GetRange (lp, i - lp)), width);
 					foreach (var line in wrappedLines) {
 						lineResult.Add (ClipAndJustify (line, width, talign));
 					}
@@ -339,7 +359,7 @@ namespace Terminal.Gui {
 					lp = i + 1;
 				}
 			}
-			foreach (var line in WordWrap (ustring.Make (runes.GetRange(lp, runeCount - lp)), width)) {
+			foreach (var line in WordWrap (ustring.Make (runes.GetRange (lp, runeCount - lp)), width)) {
 				lineResult.Add (ClipAndJustify (line, width, talign));
 			}
 
@@ -359,7 +379,7 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Computes the maximum width needed to render the text (single line or multple lines).
+		/// Computes the maximum width needed to render the text (single line or multple lines) given a minimium width.
 		/// </summary>
 		/// <returns>Max width of lines.</returns>
 		/// <param name="text">Text, may contain newlines.</param>
@@ -528,12 +548,12 @@ namespace Terminal.Gui {
 		/// <param name="hotColor">The color to use to draw the hotkey</param>
 		public void Draw (Rect bounds, Attribute normalColor, Attribute hotColor)
 		{
-			// With this check, we protect against subclasses with overrides of Text
+			// With this check, we protect against subclasses with overrides of Text (like Button)
 			if (ustring.IsNullOrEmpty (text)) {
 				return;
 			}
 
-			Application.Driver.SetAttribute (normalColor);
+			Application.Driver?.SetAttribute (normalColor);
 
 			// Use "Lines" to ensure a Format (don't use "lines"))
 			for (int line = 0; line < Lines.Count; line++) {
@@ -558,17 +578,17 @@ namespace Terminal.Gui {
 					throw new ArgumentOutOfRangeException ();
 				}
 				for (var col = bounds.Left; col < bounds.Left + bounds.Width; col++) {
-					Application.Driver.Move (col, bounds.Top + line);
+					Application.Driver?.Move (col, bounds.Top + line);
 					var rune = (Rune)' ';
 					if (col >= x && col < (x + runes.Length)) {
 						rune = runes [col - x];
 					}
 					if ((rune & HotKeyTagMask) == HotKeyTagMask) {
-						Application.Driver.SetAttribute (hotColor);
-						Application.Driver.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
-						Application.Driver.SetAttribute (normalColor);
+						Application.Driver?.SetAttribute (hotColor);
+						Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
+						Application.Driver?.SetAttribute (normalColor);
 					} else {
-						Application.Driver.AddRune (rune);
+						Application.Driver?.AddRune (rune);
 					}
 				}
 			}

+ 8 - 2
Terminal.Gui/Core/Toplevel.cs

@@ -229,10 +229,14 @@ namespace Terminal.Gui {
 		public override void Remove (View view)
 		{
 			if (this == Application.Top) {
-				if (view is MenuBar)
+				if (view is MenuBar) {
+					MenuBar?.Dispose ();
 					MenuBar = null;
-				if (view is StatusBar)
+				}
+				if (view is StatusBar) {
 					StatusBar = null;
+					StatusBar = null;
+				}
 			}
 			base.Remove (view);
 		}
@@ -241,7 +245,9 @@ namespace Terminal.Gui {
 		public override void RemoveAll ()
 		{
 			if (this == Application.Top) {
+				MenuBar?.Dispose ();
 				MenuBar = null;
+				StatusBar?.Dispose ();
 				StatusBar = null;
 			}
 			base.RemoveAll ();

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

@@ -122,7 +122,7 @@ namespace Terminal.Gui {
 		View focused = null;
 		Direction focusDirection;
 
-		TextFormatter viewText;
+		TextFormatter textFormatter;
 
 		/// <summary>
 		/// Event fired when a subview is being added to this view.
@@ -162,12 +162,12 @@ namespace Terminal.Gui {
 		/// <summary>
 		/// Gets or sets the HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire.
 		/// </summary>
-		public Key HotKey { get => viewText.HotKey; set => viewText.HotKey = value; }
+		public Key HotKey { get => textFormatter.HotKey; set => textFormatter.HotKey = value; }
 
 		/// <summary>
 		/// Gets or sets the specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. 
 		/// </summary>
-		public Rune HotKeySpecifier { get => viewText.HotKeySpecifier; set => viewText.HotKeySpecifier = value; }
+		public Rune HotKeySpecifier { get => textFormatter.HotKeySpecifier; set => textFormatter.HotKeySpecifier = value; }
 
 		internal Direction FocusDirection {
 			get => SuperView?.FocusDirection ?? focusDirection;
@@ -391,7 +391,7 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public View (Rect frame)
 		{
-			viewText = new TextFormatter ();
+			textFormatter = new TextFormatter ();
 			this.Text = ustring.Empty;
 
 			this.Frame = frame;
@@ -454,7 +454,7 @@ namespace Terminal.Gui {
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
 		public View (Rect rect, ustring text) : this (rect)
 		{
-			viewText = new TextFormatter ();
+			textFormatter = new TextFormatter ();
 			this.Text = text;
 		}
 
@@ -474,7 +474,7 @@ namespace Terminal.Gui {
 		/// <param name="text">text to initialize the <see cref="Text"/> property with.</param>
 		public View (ustring text) : base ()
 		{
-			viewText = new TextFormatter ();
+			textFormatter = new TextFormatter ();
 			this.Text = text;
 
 			CanFocus = false;
@@ -505,7 +505,7 @@ namespace Terminal.Gui {
 			if (SuperView == null)
 				return;
 			SuperView.SetNeedsLayout ();
-			viewText.SetNeedsFormat ();
+			textFormatter.NeedsFormat = true;
 		}
 
 		/// <summary>
@@ -900,7 +900,7 @@ namespace Terminal.Gui {
 				focused.PositionCursor ();
 			else {
 				if (CanFocus && HasFocus) {
-					Move (viewText.HotKeyPos == -1 ? 1 : viewText.HotKeyPos, 0);
+					Move (textFormatter.HotKeyPos == -1 ? 1 : textFormatter.HotKeyPos, 0);
 				} else {
 					Move (frame.X, frame.Y);
 				}
@@ -1088,8 +1088,10 @@ namespace Terminal.Gui {
 			if (!ustring.IsNullOrEmpty (Text)) {
 				Clear ();
 				// Draw any Text
-				viewText?.SetNeedsFormat ();
-				viewText?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal);
+				if (textFormatter != null) {
+					textFormatter.NeedsFormat = true;
+				}
+				textFormatter?.Draw (ViewToScreen (Bounds), HasFocus ? ColorScheme.Focus : ColorScheme.Normal, HasFocus ? ColorScheme.HotFocus : ColorScheme.HotNormal);
 			}
 
 			// Invoke DrawContentEvent
@@ -1572,7 +1574,7 @@ namespace Terminal.Gui {
 			Rect oldBounds = Bounds;
 			OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds });
 
-			viewText.Size = Bounds.Size;
+			textFormatter.Size = Bounds.Size;
 
 
 			// Sort out the dependencies of the X, Y, Width, Height properties
@@ -1633,9 +1635,9 @@ namespace Terminal.Gui {
 		/// </para>
 		/// </remarks>
 		public virtual ustring Text {
-			get => viewText.Text;
+			get => textFormatter.Text;
 			set {
-				viewText.Text = value;
+				textFormatter.Text = value;
 				SetNeedsDisplay ();
 			}
 		}
@@ -1645,9 +1647,9 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <value>The text alignment.</value>
 		public virtual TextAlignment TextAlignment {
-			get => viewText.Alignment;
+			get => textFormatter.Alignment;
 			set {
-				viewText.Alignment = value;
+				textFormatter.Alignment = value;
 				SetNeedsDisplay ();
 			}
 		}
@@ -1732,5 +1734,16 @@ namespace Terminal.Gui {
 			}
 			return false;
 		}
+
+		/// <inheritdoc/>
+		protected override void Dispose (bool disposing)
+		{
+			for (int i = InternalSubviews.Count - 1; i >= 0; i--) {
+				View subview = InternalSubviews [i];
+				Remove (subview);
+				subview.Dispose ();
+			}
+			base.Dispose (disposing);
+		}
 	}
 }

+ 12 - 0
Terminal.Gui/Terminal.Gui.csproj

@@ -187,6 +187,18 @@
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
     <DebugType></DebugType>
   </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
+  </PropertyGroup>
+
+  <!--<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net472|AnyCPU'">
+    <DefineConstants>TRACE</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net472|AnyCPU'">
+    <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
+  </PropertyGroup>-->
   <ItemGroup>
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0" PrivateAssets="true" />
     <PackageReference Include="NStack.Core" Version="0.14.0" />

+ 15 - 0
Terminal.Gui/Views/Label.cs

@@ -55,6 +55,21 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public Action Clicked;
 
+		///// <inheritdoc/>
+		//public new ustring Text {
+		//	get => base.Text;
+		//	set {
+		//		base.Text = value;
+		//		// This supports Label auto-sizing when Text changes (preserving backwards compat behavior)
+		//		if (Frame.Height == 1 && !ustring.IsNullOrEmpty (value)) {
+		//			int w = Text.RuneCount;
+		//			Width = w;
+		//			Frame = new Rect (Frame.Location, new Size (w, Frame.Height));
+		//		}
+		//		SetNeedsDisplay ();
+		//	}
+		//}
+
 		/// <summary>
 		/// Method invoked when a mouse event is generated
 		/// </summary>

+ 12 - 4
Terminal.Gui/Views/Menu.cs

@@ -344,13 +344,13 @@ namespace Terminal.Gui {
 				var uncheckedChar = Driver.UnSelected;
 
 				if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) {
-					checkChar = Driver.Checked; 
+					checkChar = Driver.Checked;
 					uncheckedChar = Driver.UnChecked;
 				}
 
 				// Support Checked even though CHeckType wasn't set
 				if (item.Checked) {
-					textToDraw = ustring.Make(new Rune [] { checkChar, ' ' }) + item.Title;
+					textToDraw = ustring.Make (new Rune [] { checkChar, ' ' }) + item.Title;
 				} else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) ||
 					item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) {
 					textToDraw = ustring.Make (new Rune [] { uncheckedChar, ' ' }) + item.Title;
@@ -793,8 +793,10 @@ namespace Terminal.Gui {
 				lastFocused = lastFocused ?? SuperView.MostFocused;
 				if (openSubMenu != null)
 					CloseMenu (false, true);
-				if (openMenu != null)
+				if (openMenu != null) {
 					SuperView.Remove (openMenu);
+					openMenu.Dispose ();
+				}
 
 				for (int i = 0; i < index; i++)
 					pos += Menus [i].Title.Length + 2;
@@ -867,11 +869,13 @@ namespace Terminal.Gui {
 			OnMenuClosing ();
 			switch (isSubMenu) {
 			case false:
-				if (openMenu != null)
+				if (openMenu != null) {
 					SuperView.Remove (openMenu);
+				}
 				SetNeedsDisplay ();
 				if (previousFocused != null && openMenu != null && previousFocused.ToString () != openCurrentMenu.ToString ())
 					previousFocused?.SuperView?.SetFocus (previousFocused);
+				openMenu?.Dispose ();
 				openMenu = null;
 				if (lastFocused is Menu) {
 					lastFocused = null;
@@ -914,6 +918,7 @@ namespace Terminal.Gui {
 				if (openSubMenu != null) {
 					SuperView.Remove (openSubMenu [i]);
 					openSubMenu.Remove (openSubMenu [i]);
+					openSubMenu [i].Dispose ();
 				}
 				RemoveSubMenu (i);
 			}
@@ -948,6 +953,7 @@ namespace Terminal.Gui {
 			if (openSubMenu != null) {
 				foreach (var item in openSubMenu) {
 					SuperView.Remove (item);
+					item.Dispose ();
 				}
 			}
 		}
@@ -1063,6 +1069,7 @@ namespace Terminal.Gui {
 			if (mi.IsTopLevel) {
 				var menu = new Menu (this, i, 0, mi);
 				menu.Run (mi.Action);
+				menu.Dispose ();
 			} else {
 				openedByHotKey = true;
 				Application.GrabMouse (this);
@@ -1170,6 +1177,7 @@ namespace Terminal.Gui {
 							if (Menus [i].IsTopLevel) {
 								var menu = new Menu (this, i, 0, Menus [i]);
 								menu.Run (Menus [i].Action);
+								menu.Dispose ();
 							}
 						} else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked) {
 							if (IsMenuOpen) {

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

@@ -725,5 +725,19 @@ namespace Terminal.Gui {
 			}
 			return true;
 		}
+
+		///<inheritdoc/>
+		protected override void Dispose (bool disposing)
+		{
+			if (!showVerticalScrollIndicator) {
+				// It was not added to SuperView, so it won't get disposed automatically
+				vertical?.Dispose ();
+			}
+			if (!showHorizontalScrollIndicator) {
+				// It was not added to SuperView, so it won't get disposed automatically
+				horizontal?.Dispose ();
+			}
+			base.Dispose (disposing);
+		}
 	}
 }

+ 19 - 1
Terminal.Gui/Views/StatusBar.cs

@@ -63,6 +63,8 @@ namespace Terminal.Gui {
 	/// So for each context must be a new instance of a statusbar.
 	/// </summary>
 	public class StatusBar : View {
+		bool disposedValue;
+
 		/// <summary>
 		/// The items that compose the <see cref="StatusBar"/>
 		/// </summary>
@@ -90,7 +92,12 @@ namespace Terminal.Gui {
 			Width = Dim.Fill ();
 			Height = 1;
 
-			LayoutComplete += (e) => {
+			Application.Resized += Application_Resized ();
+		}
+
+		private Action<Application.ResizedEventArgs> Application_Resized ()
+		{
+			return delegate {
 				X = 0;
 				Height = 1;
 				if (SuperView == null || SuperView == Application.Top) {
@@ -192,5 +199,16 @@ namespace Terminal.Gui {
 				return false;
 			});
 		}
+
+		/// <inheritdoc/>
+		protected override void Dispose (bool disposing)
+		{
+			if (!disposedValue) {
+				if (disposing) {
+					Application.Resized -= Application_Resized ();
+				}
+				disposedValue = true;
+			}
+		}
 	}
 }

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

@@ -33,7 +33,7 @@ namespace Terminal.Gui {
 		public bool ReadOnly { get; set; } = false;
 
 		/// <summary>
-		///   Changed event, raised when the text has clicked.
+		///   Changed event, raised when the text has changed.
 		/// </summary>
 		/// <remarks>
 		///   This event is raised when the <see cref="Text"/> changes. 

+ 2 - 2
Terminal.Gui/Views/TimeField.cs

@@ -77,10 +77,10 @@ namespace Terminal.Gui {
 			shortFormat = $" hh\\{sepChar}mm";
 			CursorPosition = 1;
 			Time = time;
-			TextChanged += TimeField_Changed;
+			TextChanged += TextField_TextChanged;
 		}
 
-		void TimeField_Changed (ustring e)
+		void TextField_TextChanged (ustring e)
 		{
 			try {
 				if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result))

+ 11 - 1
Terminal.Gui/Windows/FileDialog.cs

@@ -439,7 +439,11 @@ namespace Terminal.Gui {
 		/// <param name="message">The message.</param>
 		public FileDialog (ustring title, ustring prompt, ustring nameFieldLabel, ustring message) : base (title)//, Driver.Cols - 20, Driver.Rows - 5, null)
 		{
-			this.message = new Label (Rect.Empty, "MESSAGE" + message);
+			this.message = new Label (message) { 
+				X = 1,
+				Y = 0,
+			};
+			Add (this.message);
 			var msgLines = TextFormatter.MaxLines (message, Driver.Cols - 20);
 
 			dirLabel = new Label ("Directory: ") {
@@ -509,6 +513,12 @@ namespace Terminal.Gui {
 			//SetFocus (nameEntry);
 		}
 
+		//protected override void Dispose (bool disposing)
+		//{
+		//	message?.Dispose ();
+		//	base.Dispose (disposing);
+		//}
+
 		/// <summary>
 		/// Gets or sets the prompt label for the <see cref="Button"/> displayed to the user
 		/// </summary>

+ 13 - 2
UICatalog/Scenarios/AllViewsTester.cs

@@ -2,6 +2,7 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
 using System.Text;
@@ -334,7 +335,9 @@ namespace UICatalog {
 		{
 			// Remove existing class, if any
 			if (view != null) {
+				view.LayoutComplete -= LayoutCompleteHandler;
 				_hostPane.Remove (view);
+				view.Dispose ();
 				_hostPane.Clear ();
 			}
 		}
@@ -346,8 +349,8 @@ namespace UICatalog {
 
 			//_curView.X = Pos.Center ();
 			//_curView.Y = Pos.Center ();
-			//_curView.Width = Dim.Fill (5);
-			//_curView.Height = Dim.Fill (5);
+			view.Width = Dim.Percent(75);
+			view.Height = Dim.Percent (75);
 
 			// Set the colorscheme to make it stand out
 			view.ColorScheme = Colors.Base;
@@ -384,9 +387,17 @@ namespace UICatalog {
 			_hostPane.SetNeedsDisplay ();
 			UpdateSettings (view);
 			UpdateTitle (view);
+
+			view.LayoutComplete += LayoutCompleteHandler;
+
 			return view;
 		}
 
+		void LayoutCompleteHandler(View.LayoutEventArgs args)
+		{
+			UpdateTitle (_curView);
+		}
+
 		public override void Run ()
 		{
 			base.Run ();

+ 10 - 5
UICatalog/Scenarios/CharacterMap.cs

@@ -16,9 +16,10 @@ namespace UICatalog {
 	[ScenarioCategory ("Text")]
 	[ScenarioCategory ("Controls")]
 	class CharacterMap : Scenario {
+		CharMap _charMap;
 		public override void Setup ()
 		{
-			var charMap = new CharMap () {
+			_charMap = new CharMap () {
 				X = 0,
 				Y = 0,
 				Width = CharMap.RowWidth + 2,
@@ -28,8 +29,8 @@ namespace UICatalog {
 				CanFocus = true,
 			};
 
-			Win.Add (charMap);
-			var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (charMap) + 1, Y = Pos.Y (charMap) };
+			Win.Add (_charMap);
+			var label = new Label ("Jump To Unicode Block:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) };
 			Win.Add (label);
 
 			(ustring radioLabel, int start, int end) CreateRadio (ustring title, int start, int end)
@@ -62,11 +63,16 @@ namespace UICatalog {
 			jumpList.Y = Pos.Bottom (label);
 			jumpList.Width = Dim.Fill ();
 			jumpList.SelectedItemChanged = (args) => {
-				charMap.Start = radioItems [args.SelectedItem].start;
+				_charMap.Start = radioItems[args.SelectedItem].start;
 			};
 
 			Win.Add (jumpList);
 		}
+
+		public override void Run ()
+		{
+			base.Run ();
+		}
 	}
 
 	class CharMap : ScrollView {
@@ -106,7 +112,6 @@ namespace UICatalog {
 
 			DrawContent += CharMap_DrawContent;
 		}
-
 #if true
 		private void CharMap_DrawContent (Rect viewport)
 		{

+ 78 - 23
UICatalog/Scenarios/Text.cs

@@ -9,51 +9,106 @@ namespace UICatalog {
 	class Text : Scenario {
 		public override void Setup ()
 		{
-			var s = "This is a test intended to show how TAB key works (or doesn't) across text fields.";
-			Win.Add (new TextField (s) {
-				X = 5,
+			var s = "TAB to jump between text fields.";
+			var textField = new TextField (s) {
+				X = 1,
 				Y = 1,
-				Width = Dim.Percent (80),
-				ColorScheme = Colors.Dialog
-			});
+				Width = Dim.Percent (50),
+				//ColorScheme = Colors.Dialog
+			};
+			Win.Add (textField);
+
+			var labelMirroringTextField = new Label (textField.Text) {
+				X = Pos.Right (textField) + 1,
+				Y = Pos.Top (textField),
+				Width = Dim.Fill (1)
+			};
+			Win.Add (labelMirroringTextField);
+
+			textField.TextChanged += (prev) => {
+				labelMirroringTextField.Text = textField.Text;
+			};
 
 			var textView = new TextView () {
-				X = 5,
+				X = 1,
 				Y = 3,
-				Width = Dim.Percent (80),
-				Height = Dim.Percent (40),
+				Width = Dim.Percent (50),
+				Height = Dim.Percent (30),
 				ColorScheme = Colors.Dialog
 			};
 			textView.Text = s;
 			Win.Add (textView);
 
+			var labelMirroringTextView = new Label (textView.Text) {
+				X = Pos.Right (textView) + 1,
+				Y = Pos.Top (textView),
+				Width = Dim.Fill (1),
+				Height = Dim.Height (textView),
+			};
+			Win.Add (labelMirroringTextView);
+
+			textView.TextChanged += () => {
+				labelMirroringTextView.Text = textView.Text;
+			};
+
 			// BUGBUG: 531 - TAB doesn't go to next control from HexView
-			var hexView = new HexView (new System.IO.MemoryStream(Encoding.ASCII.GetBytes (s))) {
-				X = 5,
-				Y = Pos.Bottom(textView) + 1,
-				Width = Dim.Percent(80),
-				Height = Dim.Percent(40),
-				ColorScheme = Colors.Dialog
+			var hexView = new HexView (new System.IO.MemoryStream (Encoding.ASCII.GetBytes (s))) {
+				X = 1,
+				Y = Pos.Bottom (textView) + 1,
+				Width = Dim.Fill (1),
+				Height = Dim.Percent (30),
+				//ColorScheme = Colors.Dialog
 			};
 			Win.Add (hexView);
 
 			var dateField = new DateField (System.DateTime.Now) {
-				X = 5,
+				X = 1,
 				Y = Pos.Bottom (hexView) + 1,
-				Width = Dim.Percent (40),
-				ColorScheme = Colors.Dialog,
+				Width = 20,
+				//ColorScheme = Colors.Dialog,
 				IsShortFormat = false
 			};
 			Win.Add (dateField);
 
-			var timeField = new TimeField (DateTime.Now.TimeOfDay) {
-				X = Pos.Right (dateField) + 5,
+			var labelMirroringDateField = new Label (dateField.Text) {
+				X = Pos.Right (dateField) + 1,
+				Y = Pos.Top (dateField),
+				Width = Dim.Width (dateField),
+				Height = Dim.Height (dateField),
+			};
+			Win.Add (labelMirroringDateField);
+
+			dateField.TextChanged += (prev) => {
+				labelMirroringDateField.Text = dateField.Text;
+			};
+
+			_timeField = new TimeField (DateTime.Now.TimeOfDay) {
+				X = Pos.Right (labelMirroringDateField) + 5,
 				Y = Pos.Bottom (hexView) + 1,
-				Width = Dim.Percent (40),
-				ColorScheme = Colors.Dialog,
+				Width = 20,
+				//ColorScheme = Colors.Dialog,
 				IsShortFormat = false
 			};
-			Win.Add (timeField);
+			Win.Add (_timeField);
+
+			_labelMirroringTimeField = new Label (_timeField.Text) {
+				X = Pos.Right (_timeField) + 1,
+				Y = Pos.Top (_timeField),
+				Width = Dim.Width (_timeField),
+				Height = Dim.Height (_timeField),
+			};
+			Win.Add (_labelMirroringTimeField);
+
+			_timeField.TimeChanged += TimeChanged;
+
+		}
+
+		TimeField _timeField;
+		Label _labelMirroringTimeField;
+
+		private void TimeChanged (DateTimeEventArgs<TimeSpan> e)
+		{
+			_labelMirroringTimeField.Text = _timeField.Text;
 
 		}
 	}

+ 18 - 6
UICatalog/Scenarios/TimeAndDate.cs

@@ -53,37 +53,49 @@ namespace UICatalog {
 
 			lblOldTime = new Label ("Old Time: ") {
 				X = Pos.Center (),
-				Y = Pos.Bottom (longDate) + 1
+				Y = Pos.Bottom (longDate) + 1,
+				TextAlignment = TextAlignment.Centered,
+				Width = Dim.Fill(),
 			};
 			Win.Add (lblOldTime);
 
 			lblNewTime = new Label ("New Time: ") {
 				X = Pos.Center (),
-				Y = Pos.Bottom (lblOldTime) + 1
+				Y = Pos.Bottom (lblOldTime) + 1,
+				TextAlignment = TextAlignment.Centered,
+				Width = Dim.Fill (),
 			};
 			Win.Add (lblNewTime);
 
 			lblTimeFmt = new Label ("Time Format: ") {
 				X = Pos.Center (),
-				Y = Pos.Bottom (lblNewTime) + 1
+				Y = Pos.Bottom (lblNewTime) + 1,
+				TextAlignment = TextAlignment.Centered,
+				Width = Dim.Fill (),
 			};
 			Win.Add (lblTimeFmt);
 
 			lblOldDate = new Label ("Old Date: ") {
 				X = Pos.Center (),
-				Y = Pos.Bottom (lblTimeFmt) + 2
+				Y = Pos.Bottom (lblTimeFmt) + 2,
+				TextAlignment = TextAlignment.Centered,
+				Width = Dim.Fill (),
 			};
 			Win.Add (lblOldDate);
 
 			lblNewDate = new Label ("New Date: ") {
 				X = Pos.Center (),
-				Y = Pos.Bottom (lblOldDate) + 1
+				Y = Pos.Bottom (lblOldDate) + 1,
+				TextAlignment = TextAlignment.Centered,
+				Width = Dim.Fill (),
 			};
 			Win.Add (lblNewDate);
 
 			lblDateFmt = new Label ("Date Format: ") {
 				X = Pos.Center (),
-				Y = Pos.Bottom (lblNewDate) + 1
+				Y = Pos.Bottom (lblNewDate) + 1,
+				TextAlignment = TextAlignment.Centered,
+				Width = Dim.Fill (),
 			};
 			Win.Add (lblDateFmt);
 

+ 2 - 0
UICatalog/Scenarios/TopLevelNoWindowBug.cs

@@ -8,6 +8,8 @@ namespace UICatalog {
 
 		public override void Run ()
 		{
+			Top?.Dispose ();
+
 			Top = new Toplevel (new Rect (0, 0, Application.Driver.Cols, Application.Driver.Rows));
 
 			var menu = new MenuBar (new MenuBarItem [] {

+ 3 - 1
UICatalog/Scenarios/Unicode.cs

@@ -56,6 +56,8 @@ namespace UICatalog {
 			var checkBox = new CheckBox (gitString) { X = 20, Y = Pos.Y (label), Width = Dim.Percent (50) };
 			Win.Add (checkBox);
 
+			// BUGBUG: Combobox does not deal with unicode properly. 
+#if false
 			label = new Label ("ComboBox:") { X = Pos.X (label), Y = Pos.Bottom (label) + 1 };
 			Win.Add (label);
 			var comboBox = new ComboBox () {
@@ -67,7 +69,7 @@ namespace UICatalog {
 
 			Win.Add (comboBox);
 			comboBox.Text = gitString;
-
+#endif
 			label = new Label ("HexView:") { X = Pos.X (label), Y = Pos.Bottom (label) + 2 };
 			Win.Add (label);
 			var hexView = new HexView (new System.IO.MemoryStream (Encoding.ASCII.GetBytes (gitString + " Со_хранить"))) {

+ 111 - 100
UICatalog/UICatalog.cs

@@ -78,17 +78,42 @@ namespace UICatalog {
 				return;
 			}
 
-			Scenario scenario = GetScenarioToRun ();
-			while (scenario != null) {
+			Scenario scenario;
+			while ((scenario = GetScenarioToRun ()) != null) {
+#if DEBUG_IDISPOSABLE
+				// Validate there are no outstanding Responder-based instances 
+				// after a sceanario was selected to run. This proves the main UI Catalog
+				// 'app' closed cleanly.
+				foreach (var inst in Responder.Instances) {
+					Debug.Assert (inst.WasDisposed);
+				}
+				Responder.Instances.Clear ();
+#endif
+
 				Application.UseSystemConsole = _useSystemConsole;
 				Application.Init ();
 				scenario.Init (Application.Top, _baseColorScheme);
 				scenario.Setup ();
 				scenario.Run ();
-				scenario = GetScenarioToRun ();
+
+#if DEBUG_IDISPOSABLE
+				// After the scenario runs, validate all Responder-based instances
+				// were disposed. This proves the scenario 'app' closed cleanly.
+				foreach (var inst in Responder.Instances) {
+					Debug.Assert (inst.WasDisposed);
+				}
+				Responder.Instances.Clear();
+#endif
+			}
+
+#if DEBUG_IDISPOSABLE
+			// This proves that when the user exited the UI Catalog app
+			// it cleaned up properly.
+			foreach (var inst in Responder.Instances) {
+				Debug.Assert (inst.WasDisposed);
 			}
-			if (!_top.Running)
-				Application.Shutdown (true);
+			Responder.Instances.Clear ();
+#endif
 		}
 
 		/// <summary>
@@ -100,101 +125,6 @@ namespace UICatalog {
 			Application.UseSystemConsole = false;
 			Application.Init ();
 
-			if (_menu == null) {
-				Setup ();
-			}
-
-			_top = Application.Top;
-
-			_top.KeyDown += KeyDownHandler;
-
-			_top.Add (_menu);
-			_top.Add (_leftPane);
-			_top.Add (_rightPane);
-			_top.Add (_statusBar);
-
-			_top.Ready += () => {
-				if (_runningScenario != null) {
-					_top.SetFocus (_rightPane);
-					_runningScenario = null;
-				}
-			};
-
-			Application.Run (_top, false);
-			Application.Shutdown (false);
-			return _runningScenario;
-		}
-
-		static MenuItem [] CreateDiagnosticMenuItems ()
-		{
-			MenuItem CheckedMenuMenuItem (ustring menuItem, Action action, Func<bool> checkFunction)
-			{
-				var mi = new MenuItem ();
-				mi.Title = menuItem;
-				mi.CheckType |= MenuItemCheckStyle.Checked;
-				mi.Checked = checkFunction ();
-				mi.Action = () => {
-					action?.Invoke ();
-					mi.Title = menuItem;
-					mi.Checked = checkFunction ();
-				};
-				return mi;
-			}
-
-			return new MenuItem [] {
-				CheckedMenuMenuItem ("Use _System Console",
-					() => {
-						_useSystemConsole = !_useSystemConsole;
-					},
-					() => _useSystemConsole),
-				CheckedMenuMenuItem ("Diagnostics: _Frame Padding",
-					() => {
-						ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding;
-						_top.SetNeedsDisplay ();
-					},
-					() => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding),
-				CheckedMenuMenuItem ("Diagnostics: Frame _Ruler",
-					() => {
-						ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler;
-						_top.SetNeedsDisplay ();
-					},
-					() => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler),
-			};
-		}
-
-		static void SetColorScheme ()
-		{
-			_leftPane.ColorScheme = _baseColorScheme;
-			_rightPane.ColorScheme = _baseColorScheme;
-			_top?.SetNeedsDisplay ();
-		}
-
-		static ColorScheme _baseColorScheme;
-		static MenuItem [] CreateColorSchemeMenuItems ()
-		{
-			List<MenuItem> menuItems = new List<MenuItem> ();
-			foreach (var sc in Colors.ColorSchemes) {
-				var item = new MenuItem ();
-				item.Title = sc.Key;
-				item.CheckType |= MenuItemCheckStyle.Radio;
-				item.Checked = sc.Value == _baseColorScheme;
-				item.Action += () => {
-					_baseColorScheme = sc.Value;
-					SetColorScheme ();
-					foreach (var menuItem in menuItems) {
-						menuItem.Checked = menuItem.Title.Equals (sc.Key) && sc.Value == _baseColorScheme;
-					}
-				};
-				menuItems.Add (item);
-			}
-			return menuItems.ToArray ();
-		}
-
-		/// <summary>
-		/// Create all controls. This gets called once and the controls remain with their state between Sceanrio runs.
-		/// </summary>
-		private static void Setup ()
-		{
 			// Set this here because not initilzied until driver is loaded
 			_baseColorScheme = Colors.Base;
 
@@ -284,6 +214,87 @@ namespace UICatalog {
 			});
 
 			SetColorScheme ();
+			_top = Application.Top;
+			_top.KeyDown += KeyDownHandler;
+			_top.Add (_menu);
+			_top.Add (_leftPane);
+			_top.Add (_rightPane);
+			_top.Add (_statusBar);
+			_top.Ready += () => {
+				if (_runningScenario != null) {
+					_top.SetFocus (_rightPane);
+					_runningScenario = null;
+				}
+			};
+
+			Application.Run (_top, true);
+			Application.Shutdown ();
+			return _runningScenario;
+		}
+
+		static MenuItem [] CreateDiagnosticMenuItems ()
+		{
+			MenuItem CheckedMenuMenuItem (ustring menuItem, Action action, Func<bool> checkFunction)
+			{
+				var mi = new MenuItem ();
+				mi.Title = menuItem;
+				mi.CheckType |= MenuItemCheckStyle.Checked;
+				mi.Checked = checkFunction ();
+				mi.Action = () => {
+					action?.Invoke ();
+					mi.Title = menuItem;
+					mi.Checked = checkFunction ();
+				};
+				return mi;
+			}
+
+			return new MenuItem [] {
+				CheckedMenuMenuItem ("Use _System Console",
+					() => {
+						_useSystemConsole = !_useSystemConsole;
+					},
+					() => _useSystemConsole),
+				CheckedMenuMenuItem ("Diagnostics: _Frame Padding",
+					() => {
+						ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FramePadding;
+						_top.SetNeedsDisplay ();
+					},
+					() => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FramePadding) == ConsoleDriver.DiagnosticFlags.FramePadding),
+				CheckedMenuMenuItem ("Diagnostics: Frame _Ruler",
+					() => {
+						ConsoleDriver.Diagnostics ^= ConsoleDriver.DiagnosticFlags.FrameRuler;
+						_top.SetNeedsDisplay ();
+					},
+					() => (ConsoleDriver.Diagnostics & ConsoleDriver.DiagnosticFlags.FrameRuler) == ConsoleDriver.DiagnosticFlags.FrameRuler),
+			};
+		}
+
+		static void SetColorScheme ()
+		{
+			_leftPane.ColorScheme = _baseColorScheme;
+			_rightPane.ColorScheme = _baseColorScheme;
+			_top?.SetNeedsDisplay ();
+		}
+
+		static ColorScheme _baseColorScheme;
+		static MenuItem [] CreateColorSchemeMenuItems ()
+		{
+			List<MenuItem> menuItems = new List<MenuItem> ();
+			foreach (var sc in Colors.ColorSchemes) {
+				var item = new MenuItem ();
+				item.Title = sc.Key;
+				item.CheckType |= MenuItemCheckStyle.Radio;
+				item.Checked = sc.Value == _baseColorScheme;
+				item.Action += () => {
+					_baseColorScheme = sc.Value;
+					SetColorScheme ();
+					foreach (var menuItem in menuItems) {
+						menuItem.Checked = menuItem.Title.Equals (sc.Key) && sc.Value == _baseColorScheme;
+					}
+				};
+				menuItems.Add (item);
+			}
+			return menuItems.ToArray ();
 		}
 
 		private static void _scenarioListView_OpenSelectedItem (EventArgs e)

+ 8 - 0
UICatalog/UICatalog.csproj

@@ -8,6 +8,14 @@
     <LangVersion>8.0</LangVersion>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <DefineConstants>TRACE</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
+  </PropertyGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
   </ItemGroup>

+ 7 - 0
UnitTests/ApplicationTests.cs

@@ -12,6 +12,13 @@ using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui {
 	public class ApplicationTests {
+		public ApplicationTests ()
+		{
+#if DEBUG_IDISPOSABLE
+			Responder.Instances.Clear ();
+#endif
+		}
+
 		[Fact]
 		public void Init_Shutdown_Cleans_Up ()
 		{

+ 7 - 0
UnitTests/ResponderTests.cs

@@ -35,5 +35,12 @@ namespace Terminal.Gui {
 			Assert.False (r.OnEnter (new View ()));
 			Assert.False (r.OnLeave (new View ()));
 		}
+
+		// Generic lifetime (IDisposable) tests
+		[Fact]
+		public void Dispose_Works ()
+		{
+
+		}
 	}
 }

+ 20 - 0
UnitTests/ScenarioTests.cs

@@ -10,6 +10,13 @@ using Console = Terminal.Gui.FakeConsole;
 
 namespace Terminal.Gui {
 	public class ScenarioTests {
+		public ScenarioTests ()
+		{
+#if DEBUG_IDISPOSABLE
+			Responder.Instances.Clear ();
+#endif
+		}
+
 		int CreateInput (string input)
 		{
 			// Put a control-q in at the end
@@ -71,6 +78,12 @@ namespace Terminal.Gui {
 				Assert.Equal (1, iterations);
 				Assert.Equal (stackSize, iterations);
 			}
+#if DEBUG_IDISPOSABLE
+			foreach (var inst in Responder.Instances) {
+				Assert.True (inst.WasDisposed);
+			}
+			Responder.Instances.Clear ();
+#endif
 		}
 
 		[Fact]
@@ -119,6 +132,13 @@ namespace Terminal.Gui {
 			// # of key up events should match # of iterations
 			//Assert.Equal (1, iterations);
 			Assert.Equal (stackSize, iterations);
+
+#if DEBUG_IDISPOSABLE
+			foreach (var inst in Responder.Instances) {
+				Assert.True (inst.WasDisposed);
+			}
+			Responder.Instances.Clear ();
+#endif
 		}
 	}
 }

+ 66 - 0
UnitTests/TextFormatterTests.cs

@@ -16,10 +16,76 @@ namespace Terminal.Gui {
 		[Fact]
 		public void Basic_Usage ()
 		{
+			var testText = ustring.Make("test");
+			var expectedSize = new Size ();
+			var testBounds = new Rect (0, 0, 100, 1);
 			var tf = new TextFormatter ();
 
+			tf.Text = testText;
+			expectedSize = new Size (testText.Length, 1);
+			Assert.Equal (testText, tf.Text);
+			Assert.Equal (TextAlignment.Left, tf.Alignment);
+			Assert.Equal (expectedSize, tf.Size);
+			tf.Draw (testBounds, new Attribute(), new Attribute());
+			Assert.Equal (expectedSize, tf.Size);
+			Assert.NotEmpty (tf.Lines);
+
+			tf.Alignment = TextAlignment.Right;
+			expectedSize = new Size (testText.Length, 1);
+			Assert.Equal (testText, tf.Text);
+			Assert.Equal (TextAlignment.Right, tf.Alignment);
+			Assert.Equal (expectedSize, tf.Size);
+			tf.Draw (testBounds, new Attribute (), new Attribute ());
+			Assert.Equal (expectedSize, tf.Size);
+			Assert.NotEmpty (tf.Lines);
+
+			tf.Alignment = TextAlignment.Right;
+			expectedSize = new Size (testText.Length * 2, 1);
+			tf.Size = expectedSize;
+			Assert.Equal (testText, tf.Text);
+			Assert.Equal (TextAlignment.Right, tf.Alignment);
+			Assert.Equal (expectedSize, tf.Size);
+			tf.Draw (testBounds, new Attribute (), new Attribute ());
+			Assert.Equal (expectedSize, tf.Size);
+			Assert.NotEmpty (tf.Lines);
+
+			tf.Alignment = TextAlignment.Centered;
+			expectedSize = new Size (testText.Length * 2, 1);
+			tf.Size = expectedSize;
+			Assert.Equal (testText, tf.Text);
+			Assert.Equal (TextAlignment.Centered, tf.Alignment);
+			Assert.Equal (expectedSize, tf.Size);
+			tf.Draw (testBounds, new Attribute (), new Attribute ());
+			Assert.Equal (expectedSize, tf.Size);
+			Assert.NotEmpty (tf.Lines);
+		}
+
+		[Fact]
+		public void NeedsFormat_Sets ()
+		{
+			var testText = ustring.Make ("test");
+			var testBounds = new Rect (0, 0, 100, 1);
+			var tf = new TextFormatter ();
 
+			tf.Text = "test";
+			Assert.True (tf.NeedsFormat); // get_Lines causes a Format
+			Assert.NotEmpty (tf.Lines);
+			Assert.False (tf.NeedsFormat); // get_Lines causes a Format
+			Assert.Equal (testText, tf.Text);
+			tf.Draw (testBounds, new Attribute (), new Attribute ());
+			Assert.False (tf.NeedsFormat);
+
+			tf.Size = new Size (1, 1);
+			Assert.True (tf.NeedsFormat); 
+			Assert.NotEmpty (tf.Lines);
+			Assert.False (tf.NeedsFormat); // get_Lines causes a Format
+
+			tf.Alignment = TextAlignment.Centered;
+			Assert.True (tf.NeedsFormat);
+			Assert.NotEmpty (tf.Lines);
+			Assert.False (tf.NeedsFormat); // get_Lines causes a Format
 		}
+
 		[Fact]
 		public void FindHotKey_Invalid_ReturnsFalse ()
 		{

+ 8 - 0
UnitTests/UnitTests.csproj

@@ -5,6 +5,14 @@
     <IsPackable>false</IsPackable>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <DefineConstants>TRACE</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <DefineConstants>TRACE;DEBUG_IDISPOSABLE</DefineConstants>
+  </PropertyGroup>
+
   <ItemGroup>
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
     <PackageReference Include="System.Collections" Version="4.3.0" />