Charlie Kindel 5 years ago
parent
commit
a0522be496

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

+ 17 - 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 the view gets focus.
@@ -152,12 +152,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;
@@ -381,7 +381,7 @@ namespace Terminal.Gui {
 		/// </remarks>
 		public View (Rect frame)
 		{
-			viewText = new TextFormatter ();
+			textFormatter = new TextFormatter ();
 			this.Text = ustring.Empty;
 
 			this.Frame = frame;
@@ -444,7 +444,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;
 		}
 
@@ -464,7 +464,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;
@@ -495,7 +495,7 @@ namespace Terminal.Gui {
 			if (SuperView == null)
 				return;
 			SuperView.SetNeedsLayout ();
-			viewText.SetNeedsFormat ();
+			textFormatter.NeedsFormat = true;
 		}
 
 		/// <summary>
@@ -888,7 +888,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);
 				}
@@ -1048,8 +1048,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
@@ -1531,7 +1533,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
@@ -1592,9 +1594,9 @@ namespace Terminal.Gui {
 		/// </para>
 		/// </remarks>
 		public virtual ustring Text {
-			get => viewText.Text;
+			get => textFormatter.Text;
 			set {
-				viewText.Text = value;
+				textFormatter.Text = value;
 				SetNeedsDisplay ();
 			}
 		}
@@ -1604,9 +1606,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 ();
 			}
 		}

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

+ 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 - 2
UICatalog/Scenarios/AllViewsTester.cs

@@ -335,6 +335,7 @@ namespace UICatalog {
 		{
 			// Remove existing class, if any
 			if (view != null) {
+				view.LayoutComplete -= LayoutCompleteHandler;
 				_hostPane.Remove (view);
 				view.Dispose ();
 				_hostPane.Clear ();
@@ -348,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;
@@ -386,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 ();

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

+ 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 ()
 		{