Browse Source

Fixes #1769. Supports a minimum view size for non-automatic size views. (#1771)

* Fixes #1769. Supports a minimum view size for non-automatic size views.

* Only sets the minimum view size if it has text for non-autosize views.

* Added Dim unit tests for views with text.
BDisp 3 năm trước cách đây
mục cha
commit
b33b658f5b

+ 38 - 9
Terminal.Gui/Core/TextFormatter.cs

@@ -309,7 +309,7 @@ namespace Terminal.Gui {
 		public List<ustring> Lines {
 			get {
 				// With this check, we protect against subclasses with overrides of Text
-				if (ustring.IsNullOrEmpty (Text)) {
+				if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
 					lines = new List<ustring> ();
 					lines.Add (ustring.Empty);
 					NeedsFormat = false;
@@ -323,15 +323,18 @@ namespace Terminal.Gui {
 						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");
-					}
 
 					if (IsVerticalDirection (textDirection)) {
-						lines = Format (shown_text, Size.Height, textVerticalAlignment == VerticalTextAlignment.Justified, Size.Width > 1,
+						var colsWidth = GetSumMaxCharWidth (shown_text, 0, 1);
+						lines = Format (shown_text, Size.Height, textVerticalAlignment == VerticalTextAlignment.Justified, Size.Width > colsWidth,
 							false, 0, textDirection);
-						if (!AutoSize && lines.Count > Size.Width) {
-							lines.RemoveRange (Size.Width, lines.Count - Size.Width);
+						if (!AutoSize) {
+							colsWidth = GetMaxColsForWidth (lines, Size.Width);
+							if (lines.Count > colsWidth) {
+								for (int i = colsWidth; i < lines.Count; i++) {
+									lines.Remove (lines [i]);
+								}
+							}
 						}
 					} else {
 						lines = Format (shown_text, Size.Width, textAlignment == TextAlignment.Justified, Size.Height > 1,
@@ -576,11 +579,15 @@ namespace Terminal.Gui {
 			var runes = text.ToRuneList ();
 			int slen = runes.Count;
 			if (slen > width) {
-				return ustring.Make (runes.GetRange (0, GetMaxLengthForWidth (text, width)));
+				if (IsHorizontalDirection (textDirection)) {
+					return ustring.Make (runes.GetRange (0, GetMaxLengthForWidth (text, width)));
+				} else {
+					return ustring.Make (runes.GetRange (0, width));
+				}
 			} else {
 				if (justify) {
 					return Justify (text, width, ' ', textDirection);
-				} else if (GetTextWidth (text) > width && IsHorizontalDirection (textDirection)) {
+				} else if (IsHorizontalDirection (textDirection) && GetTextWidth (text) > width) {
 					return ustring.Make (runes.GetRange (0, GetMaxLengthForWidth (text, width)));
 				}
 				return text;
@@ -846,6 +853,28 @@ namespace Terminal.Gui {
 			return runeIdx;
 		}
 
+		/// <summary>
+		/// Gets the index position from the list based on the <paramref name="width"/>.
+		/// </summary>
+		/// <param name="lines">The lines.</param>
+		/// <param name="width">The width.</param>
+		/// <returns>The index of the list that fit the width.</returns>
+		public static int GetMaxColsForWidth (List<ustring> lines, int width)
+		{
+			var runesLength = 0;
+			var lineIdx = 0;
+			for (; lineIdx < lines.Count; lineIdx++) {
+				var runes = lines [lineIdx].ToRuneList ();
+				var maxRruneWidth = runes.Count > 0
+					? runes.Max (r => Math.Max (Rune.ColumnWidth (r), 1)) : 1;
+				if (runesLength + maxRruneWidth > width) {
+					break;
+				}
+				runesLength += maxRruneWidth;
+			}
+			return lineIdx;
+		}
+
 		/// <summary>
 		///  Calculates the rectangle required to hold text, assuming no word wrapping.
 		/// </summary>

+ 31 - 1
Terminal.Gui/Core/View.cs

@@ -571,6 +571,7 @@ namespace Terminal.Gui {
 				if (autoSize && value.Anchor (0) != TextFormatter.Size.Width) {
 					autoSize = false;
 				}
+				SetMinWidthHeight ();
 				SetNeedsLayout ();
 				if (width is Dim.DimAbsolute) {
 					frame = new Rect (frame.X, frame.Y, width.Anchor (0), frame.Height);
@@ -596,6 +597,7 @@ namespace Terminal.Gui {
 				if (autoSize && value.Anchor (0) != TextFormatter.Size.Height) {
 					autoSize = false;
 				}
+				SetMinWidthHeight ();
 				SetNeedsLayout ();
 				if (height is Dim.DimAbsolute) {
 					frame = new Rect (frame.X, frame.Y, frame.Width, height.Anchor (0));
@@ -618,6 +620,25 @@ namespace Terminal.Gui {
 			return false;
 		}
 
+		void SetMinWidthHeight ()
+		{
+			if (IsInitialized && !AutoSize && !ustring.IsNullOrEmpty (TextFormatter.Text)) {
+				switch (TextFormatter.IsVerticalDirection (TextDirection)) {
+				case true:
+					var colWidth = TextFormatter.GetSumMaxCharWidth (TextFormatter.Text, 0, 1);
+					if (Width == null || (Width is Dim.DimAbsolute && Width.Anchor (0) < colWidth)) {
+						width = colWidth;
+					}
+					break;
+				default:
+					if (Height == null || (Height is Dim.DimAbsolute && Height.Anchor (0) == 0)) {
+						height = 1;
+					}
+					break;
+				}
+			}
+		}
+
 		/// <summary>
 		/// Gets or sets the <see cref="Terminal.Gui.TextFormatter"/> which can be handled differently by any derived class.
 		/// </summary>
@@ -2288,6 +2309,7 @@ namespace Terminal.Gui {
 				} else if (!canResize && TextFormatter.Size != Bounds.Size) {
 					TextFormatter.Size = Bounds.Size;
 				}
+				SetMinWidthHeight ();
 				SetNeedsLayout ();
 				SetNeedsDisplay (new Rect (new Point (0, 0),
 					new Size (Math.Max (frame.Width, prevSize.Width), Math.Max (frame.Height, prevSize.Height))));
@@ -2359,11 +2381,19 @@ namespace Terminal.Gui {
 			}
 		}
 
+		bool isInitialized;
+
 		/// <summary>
 		/// Get or sets if  the <see cref="View"/> was already initialized.
 		/// This derived from <see cref="ISupportInitializeNotification"/> to allow notify all the views that are being initialized.
 		/// </summary>
-		public virtual bool IsInitialized { get; set; }
+		public virtual bool IsInitialized {
+			get => isInitialized;
+			set {
+				isInitialized = value;
+				SetMinWidthHeight ();
+			}
+		}
 
 		bool oldEnabled;
 

+ 112 - 0
UnitTests/DimTests.cs

@@ -641,6 +641,55 @@ namespace Terminal.Gui.Core {
 			Application.Shutdown ();
 		}
 
+		[Fact]
+		public void Dim_Add_Operator_With_Text ()
+		{
+
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+
+			var view = new View ("View") { X = 0, Y = 0, Width = 20, Height = 0 };
+			var field = new TextField () { X = 0, Y = Pos.Bottom (view), Width = 20 };
+			var count = 0;
+
+			field.KeyDown += (k) => {
+				if (k.KeyEvent.Key == Key.Enter) {
+					field.Text = $"Label {count}";
+					var label = new Label (field.Text) { X = 0, Y = view.Bounds.Height, Width = 20 };
+					view.Add (label);
+					Assert.Equal ($"Label {count}", label.Text);
+					Assert.Equal ($"Pos.Absolute({count + 1})", label.Y.ToString ());
+
+					Assert.Equal ($"Dim.Absolute({count + 1})", view.Height.ToString ());
+					view.Height += 1;
+					count++;
+					Assert.Equal ($"Dim.Absolute({count + 1})", view.Height.ToString ());
+				}
+			};
+
+			Application.Iteration += () => {
+				while (count < 20) {
+					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				}
+
+				Application.RequestStop ();
+			};
+
+			var win = new Window ();
+			win.Add (view);
+			win.Add (field);
+
+			top.Add (win);
+
+			Application.Run (top);
+
+			Assert.Equal (20, count);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
+
 		[Fact]
 		public void Dim_Subtract_Operator ()
 		{
@@ -701,6 +750,69 @@ namespace Terminal.Gui.Core {
 			Application.Shutdown ();
 		}
 
+		[Fact]
+		public void Dim_Subtract_Operator_With_Text ()
+		{
+
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+
+			var view = new View ("View") { X = 0, Y = 0, Width = 20, Height = 0 };
+			var field = new TextField () { X = 0, Y = Pos.Bottom (view), Width = 20 };
+			var count = 20;
+			var listLabels = new List<Label> ();
+
+			for (int i = 0; i < count; i++) {
+				field.Text = $"Label {i}";
+				var label = new Label (field.Text) { X = 0, Y = view.Bounds.Height, Width = 20 };
+				view.Add (label);
+				Assert.Equal ($"Label {i}", label.Text);
+				Assert.Equal ($"Pos.Absolute({i})", label.Y.ToString ());
+				listLabels.Add (label);
+
+				Assert.Equal ($"Dim.Absolute({i})", view.Height.ToString ());
+				view.Height += 1;
+				Assert.Equal ($"Dim.Absolute({i + 1})", view.Height.ToString ());
+			}
+
+			field.KeyDown += (k) => {
+				if (k.KeyEvent.Key == Key.Enter) {
+					Assert.Equal ($"Label {count - 1}", listLabels [count - 1].Text);
+					view.Remove (listLabels [count - 1]);
+
+					Assert.Equal ($"Dim.Absolute({count})", view.Height.ToString ());
+					view.Height -= 1;
+					count--;
+					if (count == 0)
+						Assert.Equal ($"Dim.Absolute({count + 1})", view.Height.ToString ());
+					else
+						Assert.Equal ($"Dim.Absolute({count})", view.Height.ToString ());
+				}
+			};
+
+			Application.Iteration += () => {
+				while (count > 0) {
+					field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ()));
+				}
+
+				Application.RequestStop ();
+			};
+
+			var win = new Window ();
+			win.Add (view);
+			win.Add (field);
+
+			top.Add (win);
+
+			Application.Run (top);
+
+			Assert.Equal (0, count);
+
+			// Shutdown must be called to safely clean up Application if Init has been called
+			Application.Shutdown ();
+		}
+
 		[Fact]
 		public void Internal_Tests ()
 		{

+ 9 - 8
UnitTests/ScenarioTests.cs

@@ -48,16 +48,16 @@ namespace Terminal.Gui {
 			List<Type> scenarioClasses = Scenario.GetDerivedClasses<Scenario> ();
 			Assert.NotEmpty (scenarioClasses);
 
-			foreach (var scenarioClass in scenarioClasses) {
+			lock (FakeConsole.MockKeyPresses) {
 
-				// Setup some fake keypresses 
-				// Passing empty string will cause just a ctrl-q to be fired
-				FakeConsole.MockKeyPresses.Clear ();
-				int stackSize = CreateInput ("");
+				foreach (var scenarioClass in scenarioClasses) {
 
-				Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+					// Setup some fake keypresses 
+					// Passing empty string will cause just a ctrl-q to be fired
+					FakeConsole.MockKeyPresses.Clear ();
+					int stackSize = CreateInput ("");
 
-				lock (FakeConsole.MockKeyPresses) {
+					Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
 
 					int iterations = 0;
 					Application.Iteration = () => {
@@ -97,7 +97,8 @@ namespace Terminal.Gui {
 
 					if (abortCount != 0) {
 						output.WriteLine ($"Scenario {scenarioClass} had abort count of {abortCount}");
-					} else if (iterations > 1) {
+					}
+					if (iterations > 1) {
 						output.WriteLine ($"Scenario {scenarioClass} had iterations count of {iterations}");
 					}
 

+ 351 - 1
UnitTests/TextFormatterTests.cs

@@ -69,7 +69,7 @@ namespace Terminal.Gui.Core {
 		public void TestSize_TextChange ()
 		{
 			var tf = new TextFormatter () { Text = "你" };
-			Assert.Equal (2,tf.Size.Width);
+			Assert.Equal (2, tf.Size.Width);
 			tf.Text = "你你";
 			Assert.Equal (4, tf.Size.Width);
 		}
@@ -3202,5 +3202,355 @@ e
 			list = TextFormatter.Format (text, 3, false, false);
 			Assert.Equal ("デ", list [^1].ToString ());
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Size_View_IsEmpty_False_Return_Null_Lines ()
+		{
+			var text = "Views";
+			var view = new View () {
+				Width = Dim.Fill () - text.Length,
+				Height = 1,
+				Text = text
+			};
+			var win = new Window ("Window") {
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			win.Add (view);
+			Application.Top.Add (win);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			Assert.Equal (5, text.Length);
+			Assert.False (view.AutoSize);
+			Assert.Equal (new Rect (0, 0, 3, 1), view.Frame);
+			Assert.Equal (new Size (3, 1), view.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { "Vie" }, view.TextFormatter.Lines);
+			Assert.Equal (new Rect (0, 0, 10, 4), win.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 4), Application.Top.Frame);
+			var expected = @"
+┌ Wind ──┐
+│Vie     │
+│        │
+└────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+
+			text = "0123456789";
+			Assert.Equal (10, text.Length);
+			view.Width = Dim.Fill () - text.Length;
+			Application.Refresh ();
+
+			Assert.Equal (new Rect (0, 0, 0, 1), view.Frame);
+			Assert.Equal (new Size (0, 1), view.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { ustring.Empty }, view.TextFormatter.Lines);
+			expected = @"
+┌ Wind ──┐
+│        │
+│        │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Size_View_IsEmpty_True_Minimum_Height ()
+		{
+			var text = "Views";
+			var view = new View () {
+				Width = Dim.Fill () - text.Length,
+				Text = text
+			};
+			var win = new Window ("Window") {
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			win.Add (view);
+			Application.Top.Add (win);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			Assert.Equal (5, text.Length);
+			Assert.False (view.AutoSize);
+			Assert.Equal (new Rect (0, 0, 3, 1), view.Frame);
+			Assert.Equal (new Size (3, 1), view.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { "Vie" }, view.TextFormatter.Lines);
+			Assert.Equal (new Rect (0, 0, 10, 4), win.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 4), Application.Top.Frame);
+			var expected = @"
+┌ Wind ──┐
+│Vie     │
+│        │
+└────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+
+			text = "0123456789";
+			Assert.Equal (10, text.Length);
+			view.Width = Dim.Fill () - text.Length;
+			Application.Refresh ();
+
+			Assert.Equal (new Rect (0, 0, 0, 1), view.Frame);
+			Assert.Equal (new Size (0, 1), view.TextFormatter.Size);
+			var exception = Record.Exception (() => Assert.Equal (new List<ustring> () { ustring.Empty }, view.TextFormatter.Lines));
+			Assert.Null (exception);
+			expected = @"
+┌ Wind ──┐
+│        │
+│        │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Size_Label_IsEmpty_False_Return_Null_Lines ()
+		{
+			var text = "Label";
+			var label = new Label () {
+				Width = Dim.Fill () - text.Length,
+				Height = 1,
+				Text = text
+			};
+			var win = new Window ("Window") {
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			win.Add (label);
+			Application.Top.Add (win);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			Assert.Equal (5, text.Length);
+			Assert.False (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, 3, 1), label.Frame);
+			Assert.Equal (new Size (3, 1), label.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { "Lab" }, label.TextFormatter.Lines);
+			Assert.Equal (new Rect (0, 0, 10, 4), win.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 4), Application.Top.Frame);
+			var expected = @"
+┌ Wind ──┐
+│Lab     │
+│        │
+└────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+
+			text = "0123456789";
+			Assert.Equal (10, text.Length);
+			label.Width = Dim.Fill () - text.Length;
+			Application.Refresh ();
+
+			Assert.Equal (new Rect (0, 0, 0, 1), label.Frame);
+			Assert.Equal (new Size (0, 1), label.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { ustring.Empty }, label.TextFormatter.Lines);
+			expected = @"
+┌ Wind ──┐
+│        │
+│        │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Size_Label_IsEmpty_True_Minimum_Height ()
+		{
+			var text = "Label";
+			var label = new Label () {
+				Width = Dim.Fill () - text.Length,
+				Text = text
+			};
+			var win = new Window ("Window") {
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			win.Add (label);
+			Application.Top.Add (win);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			Assert.Equal (5, text.Length);
+			Assert.False (label.AutoSize);
+			Assert.Equal (new Rect (0, 0, 3, 1), label.Frame);
+			Assert.Equal (new Size (3, 1), label.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { "Lab" }, label.TextFormatter.Lines);
+			Assert.Equal (new Rect (0, 0, 10, 4), win.Frame);
+			Assert.Equal (new Rect (0, 0, 10, 4), Application.Top.Frame);
+			var expected = @"
+┌ Wind ──┐
+│Lab     │
+│        │
+└────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+
+			text = "0123456789";
+			Assert.Equal (10, text.Length);
+			label.Width = Dim.Fill () - text.Length;
+			Application.Refresh ();
+
+			Assert.Equal (new Rect (0, 0, 0, 1), label.Frame);
+			Assert.Equal (new Size (0, 1), label.TextFormatter.Size);
+			var exception = Record.Exception (() => Assert.Equal (new List<ustring> () { ustring.Empty }, label.TextFormatter.Lines));
+			Assert.Null (exception);
+			expected = @"
+┌ Wind ──┐
+│        │
+│        │
+└────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 10, 4), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Size_View_IsEmpty_True_Minimum_Width ()
+		{
+			var text = "Views";
+			var view = new View () {
+				TextDirection = TextDirection.TopBottom_LeftRight,
+				Height = Dim.Fill () - text.Length,
+				Text = text
+			};
+			var win = new Window ("Window") {
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			win.Add (view);
+			Application.Top.Add (win);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (4, 10);
+
+			Assert.Equal (5, text.Length);
+			Assert.False (view.AutoSize);
+			Assert.Equal (new Rect (0, 0, 1, 3), view.Frame);
+			Assert.Equal (new Size (1, 3), view.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { "Vie" }, view.TextFormatter.Lines);
+			Assert.Equal (new Rect (0, 0, 4, 10), win.Frame);
+			Assert.Equal (new Rect (0, 0, 4, 10), Application.Top.Frame);
+			var expected = @"
+┌──┐
+│V │
+│i │
+│e │
+│  │
+│  │
+│  │
+│  │
+│  │
+└──┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 4, 10), pos);
+
+			text = "0123456789";
+			Assert.Equal (10, text.Length);
+			view.Height = Dim.Fill () - text.Length;
+			Application.Refresh ();
+
+			Assert.Equal (new Rect (0, 0, 1, 0), view.Frame);
+			Assert.Equal (new Size (1, 0), view.TextFormatter.Size);
+			var exception = Record.Exception (() => Assert.Equal (new List<ustring> () { ustring.Empty }, view.TextFormatter.Lines));
+			Assert.Null (exception);
+			expected = @"
+┌──┐
+│  │
+│  │
+│  │
+│  │
+│  │
+│  │
+│  │
+│  │
+└──┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 4, 10), pos);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void Size_View_IsEmpty_True_Minimum_Width_Wide_Rune ()
+		{
+			var text = "界View";
+			var view = new View () {
+				TextDirection = TextDirection.TopBottom_LeftRight,
+				Height = Dim.Fill () - text.Length,
+				Text = text
+			};
+			var win = new Window ("Window") {
+				Width = Dim.Fill (),
+				Height = Dim.Fill ()
+			};
+			win.Add (view);
+			Application.Top.Add (win);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (4, 10);
+
+			Assert.Equal (5, text.Length);
+			Assert.False (view.AutoSize);
+			Assert.Equal (new Rect (0, 0, 2, 3), view.Frame);
+			Assert.Equal (new Size (2, 3), view.TextFormatter.Size);
+			Assert.Equal (new List<ustring> () { "界Vi" }, view.TextFormatter.Lines);
+			Assert.Equal (new Rect (0, 0, 4, 10), win.Frame);
+			Assert.Equal (new Rect (0, 0, 4, 10), Application.Top.Frame);
+			var expected = @"
+┌──┐
+│界│
+│V │
+│i │
+│  │
+│  │
+│  │
+│  │
+│  │
+└──┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 4, 10), pos);
+
+			text = "0123456789";
+			Assert.Equal (10, text.Length);
+			view.Height = Dim.Fill () - text.Length;
+			Application.Refresh ();
+
+			Assert.Equal (new Rect (0, 0, 2, 0), view.Frame);
+			Assert.Equal (new Size (2, 0), view.TextFormatter.Size);
+			var exception = Record.Exception (() => Assert.Equal (new List<ustring> () { ustring.Empty }, view.TextFormatter.Lines));
+			Assert.Null (exception);
+			expected = @"
+┌──┐
+│  │
+│  │
+│  │
+│  │
+│  │
+│  │
+│  │
+│  │
+└──┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 4, 10), pos);
+		}
 	}
 }

+ 86 - 0
UnitTests/ViewTests.cs

@@ -2352,5 +2352,91 @@ Y
 			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
 			Assert.Equal (Rect.Empty, pos);
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Width_Height_SetMinWidthHeight_Narrow_Wide_Runes ()
+		{
+			var text = $"First line{Environment.NewLine}Second line";
+			var horizontalView = new View () {
+				Width = 20,
+				Text = text
+			};
+			var verticalView = new View () {
+				Y = 3,
+				Height = 20,
+				Text = text,
+				TextDirection = TextDirection.TopBottom_LeftRight
+			};
+			var win = new Window () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				Text = "Window"
+			};
+			win.Add (horizontalView, verticalView);
+			Application.Top.Add (win);
+			Application.Begin (Application.Top);
+			((FakeDriver)Application.Driver).SetBufferSize (22, 22);
+
+			Assert.Equal (new Rect (0, 0, 20, 1), horizontalView.Frame);
+			Assert.Equal (new Rect (0, 3, 1, 20), verticalView.Frame);
+			var expected = @"
+┌────────────────────┐
+│First line Second li│
+│                    │
+│                    │
+│F                   │
+│i                   │
+│r                   │
+│s                   │
+│t                   │
+│                    │
+│l                   │
+│i                   │
+│n                   │
+│e                   │
+│                    │
+│S                   │
+│e                   │
+│c                   │
+│o                   │
+│n                   │
+│d                   │
+└────────────────────┘
+";
+
+			var pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 22, 22), pos);
+
+			verticalView.Text = $"最初の行{Environment.NewLine}二行目";
+			Application.Top.Redraw (Application.Top.Bounds);
+			Assert.Equal (new Rect (0, 3, 2, 20), verticalView.Frame);
+			expected = @"
+┌────────────────────┐
+│First line Second li│
+│                    │
+│                    │
+│最                  │
+│初                  │
+│の                  │
+│行                  │
+│                    │
+│二                  │
+│行                  │
+│目                  │
+│                    │
+│                    │
+│                    │
+│                    │
+│                    │
+│                    │
+│                    │
+│                    │
+│                    │
+└────────────────────┘
+";
+
+			pos = GraphViewTests.AssertDriverContentsWithFrameAre (expected, output);
+			Assert.Equal (new Rect (0, 0, 22, 22), pos);
+		}
 	}
 }