Преглед изворни кода

Fixes #1777 - Dialog button justification. Adds unit tests. (#1782)

* Fixes #1777 - Dialog button justification. Adds unit tests

* Added missing API doc

* Added tests for wide chars

* more tests

* fixed test that broke by adjusting dialog button alignment

* fixed test that broke by adjusting dialog button alignment

* Fixed Dialogs scenario crash re: Parse v TryParse
Tig Kindel пре 3 година
родитељ
комит
4a338bc693
4 измењених фајлова са 597 додато и 27 уклоњено
  1. 99 7
      Terminal.Gui/Windows/Dialog.cs
  2. 58 18
      UICatalog/Scenarios/Dialogs.cs
  3. 2 2
      UnitTests/ConsoleDriverTests.cs
  4. 438 0
      UnitTests/DialogTests.cs

+ 99 - 7
Terminal.Gui/Windows/Dialog.cs

@@ -109,24 +109,115 @@ namespace Terminal.Gui {
 			LayoutSubviews ();
 		}
 
+		// Get the width of all buttons, not including any spacing
 		internal int GetButtonsWidth ()
 		{
 			if (buttons.Count == 0) {
 				return 0;
 			}
-			return buttons.Select (b => b.Bounds.Width).Sum () + buttons.Count - 1;
+			return buttons.Select (b => b.Bounds.Width).Sum ();
 		}
+		/// <summary>
+		/// Determines the horizontal alignment of the Dialog buttons.
+		/// </summary>
+		public enum ButtonAlignments {
+			/// <summary>
+			/// Center-aligns the buttons (the default).
+			/// </summary>
+			Center = 0,
+
+			/// <summary>
+			/// Justifies the buttons
+			/// </summary>
+			Justify,
+
+			/// <summary>
+			/// Left-aligns the buttons
+			/// </summary>
+			Left,
+
+			/// <summary>
+			/// Right-aligns the buttons
+			/// </summary>
+			Right
+		}
+
+		private ButtonAlignments buttonAlignment = Dialog.ButtonAlignments.Center;
+
+		/// <summary>
+		/// Determines how the <see cref="Dialog"/> <see cref="Button"/>s are aligned along the 
+		/// bottom of the dialog. 
+		/// </summary>
+		public ButtonAlignments ButtonAlignment { get => buttonAlignment; set => buttonAlignment = value; }
 
 		void LayoutStartedHandler ()
 		{
+			if (buttons.Count == 0) return;
+
+			int shiftLeft = 0;
+
 			int buttonsWidth = GetButtonsWidth ();
+			switch (ButtonAlignment) {
+			case ButtonAlignments.Center:
+				// Center Buttons
+				shiftLeft = Math.Max ((Bounds.Width - buttonsWidth - buttons.Count - 2) / 2 + 1, 0);
+				for (int i = buttons.Count - 1; i >= 0; i--) {
+					Button button = buttons [i];
+					shiftLeft += button.Frame.Width + (i == buttons.Count - 1 ? 0 : 1);
+					button.X = Pos.AnchorEnd (shiftLeft);
+					button.Y = Pos.AnchorEnd (1);
+				}
+				break;
+
+			case ButtonAlignments.Justify:
+				// Justify Buttons
+				// leftmost and rightmost buttons are hard against edges. The rest are evenly spaced.
+
+				var spacing = (int)Math.Ceiling ((double)(Bounds.Width - buttonsWidth - 2) / (buttons.Count - 1));
+				for (int i = buttons.Count - 1; i >= 0; i--) {
+					Button button = buttons [i];
+					if (i == buttons.Count - 1) {
+						shiftLeft += button.Frame.Width;
+						button.X = Pos.AnchorEnd (shiftLeft);
+					} else {
+						if (i == 0) {
+							// first (leftmost) button - always hard flush left
+							var left = Bounds.Width - 2;
+							button.X = Pos.AnchorEnd (left);
+						} else {
+							shiftLeft += button.Frame.Width + (spacing);
+							button.X = Pos.AnchorEnd (shiftLeft);
+						}
+					}
+					button.Y = Pos.AnchorEnd (1);
+				}
+				break;
+
+			case ButtonAlignments.Left:
+				// Left Align Buttons
+				var prevButton = buttons [0];
+				prevButton.X = 0;
+				prevButton.Y = Pos.AnchorEnd (1);
+				for (int i = 1; i < buttons.Count; i++) {
+					Button button = buttons [i];
+					button.X = Pos.Right (prevButton) + 1;
+					button.Y = Pos.AnchorEnd (1);
+					prevButton = button;
+				}
+				break;
 
-			int shiftLeft = Math.Max ((Bounds.Width - buttonsWidth) / 2 - 2, 0);
-			for (int i = buttons.Count - 1; i >= 0; i--) {
-				Button button = buttons [i];
-				shiftLeft += button.Frame.Width + 1;
-				button.X = Pos.AnchorEnd (shiftLeft);
-				button.Y = Pos.AnchorEnd (1);
+			case ButtonAlignments.Right:
+				// Right align buttons
+				shiftLeft = buttons [buttons.Count - 1].Frame.Width;
+				buttons [buttons.Count - 1].X = Pos.AnchorEnd (shiftLeft);
+				buttons [buttons.Count - 1].Y = Pos.AnchorEnd (1);
+				for (int i = buttons.Count - 2; i >= 0; i--) {
+					Button button = buttons [i];
+					shiftLeft += button.Frame.Width + 1;
+					button.X = Pos.AnchorEnd (shiftLeft);
+					button.Y = Pos.AnchorEnd (1);
+				}
+				break;
 			}
 		}
 
@@ -140,5 +231,6 @@ namespace Terminal.Gui {
 			}
 			return base.ProcessKey (kb);
 		}
+
 	}
 }

+ 58 - 18
UICatalog/Scenarios/Dialogs.cs

@@ -9,13 +9,13 @@ namespace UICatalog.Scenarios {
 	[ScenarioMetadata (Name: "Dialogs", Description: "Demonstrates how to the Dialog class")]
 	[ScenarioCategory ("Dialogs")]
 	public class Dialogs : Scenario {
+		static int CODE_POINT = '你'; // We know this is a wide char
 		public override void Setup ()
 		{
 			var frame = new FrameView ("Dialog Options") {
 				X = Pos.Center (),
-				Y = 1,
-				Width = Dim.Percent (75),
-				Height = 10
+				Y = 0,
+				Width = Dim.Percent (75)
 			};
 			Win.Add (frame);
 
@@ -92,10 +92,31 @@ namespace UICatalog.Scenarios {
 			};
 			frame.Add (numButtonsEdit);
 
+			var glyphsNotWords = new CheckBox ($"Add {Char.ConvertFromUtf32(CODE_POINT)} to button text to stress wide char support", false) {
+				X = Pos.Left (numButtonsEdit),
+				Y = Pos.Bottom (label),
+				TextAlignment = Terminal.Gui.TextAlignment.Right,
+			};
+			frame.Add (glyphsNotWords);
+
+
+			label = new Label ("Button Style:") {
+				X = 0,
+				Y = Pos.Bottom (glyphsNotWords),
+				AutoSize = true,
+				TextAlignment = Terminal.Gui.TextAlignment.Right,
+			};
+			frame.Add (label);
+			var styleRadioGroup = new RadioGroup (new ustring [] { "Center", "Justify", "Left", "Right" }) {
+				X = Pos.Right (label) + 1,
+				Y = Pos.Top (label),
+			};
+			frame.Add (styleRadioGroup);
+
 			void Top_Loaded ()
 			{
 				frame.Height = Dim.Height (widthEdit) + Dim.Height (heightEdit) + Dim.Height (titleEdit)
-					+ Dim.Height (numButtonsEdit) + 2;
+					+ Dim.Height (numButtonsEdit) + Dim.Height (styleRadioGroup) + Dim.Height(glyphsNotWords) + 2;
 				Top.Loaded -= Top_Loaded;
 			}
 			Top.Loaded += Top_Loaded;
@@ -112,10 +133,14 @@ namespace UICatalog.Scenarios {
 				Y = Pos.Bottom (frame) + 5,
 				Width = 25,
 				Height = 1,
+
 				ColorScheme = Colors.Error,
 			};
+			// glyphsNotWords
+			// false:var btnText = new [] { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" };
+			// true: var btnText = new [] { "0", "\u2780", "➁", "\u2783", "\u2784", "\u2785", "\u2786", "\u2787", "\u2788", "\u2789" };
+			// \u2781 is ➁ dingbats \ufb70 is	
 
-			//var btnText = new [] { "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" };
 			var showDialogButton = new Button ("Show Dialog") {
 				X = Pos.Center (),
 				Y = Pos.Bottom (frame) + 2,
@@ -123,18 +148,26 @@ namespace UICatalog.Scenarios {
 			};
 			showDialogButton.Clicked += () => {
 				try {
-					int width = int.Parse (widthEdit.Text.ToString ());
-					int height = int.Parse (heightEdit.Text.ToString ());
-					int numButtons = int.Parse (numButtonsEdit.Text.ToString ());
+					int width = 0;
+					int.TryParse (widthEdit.Text.ToString (), out width);
+					int height = 0;
+					int.TryParse (heightEdit.Text.ToString (), out height);
+					int numButtons = 3;
+					int.TryParse (numButtonsEdit.Text.ToString (), out numButtons);
 
 					var buttons = new List<Button> ();
 					var clicked = -1;
 					for (int i = 0; i < numButtons; i++) {
-						var buttonId = i;
-						//var button = new Button (btnText [buttonId % 10],
-						//	is_default: buttonId == 0);
-						var button = new Button (NumberToWords.Convert (buttonId),
-							is_default: buttonId == 0);
+						int buttonId = i;
+						Button button = null;
+						if (glyphsNotWords.Checked) {
+							buttonId = i;
+							button = new Button (NumberToWords.Convert (buttonId) + " " + Char.ConvertFromUtf32 (buttonId + CODE_POINT),
+								is_default: buttonId == 0);
+						} else {
+							button = new Button (NumberToWords.Convert (buttonId),
+							       is_default: buttonId == 0);
+						}
 						button.Clicked += () => {
 							clicked = buttonId;
 							Application.RequestStop ();
@@ -145,17 +178,24 @@ namespace UICatalog.Scenarios {
 					// This tests dynamically adding buttons; ensuring the dialog resizes if needed and 
 					// the buttons are laid out correctly
 					var dialog = new Dialog (titleEdit.Text, width, height,
-						buttons.ToArray ());
+						buttons.ToArray ()) {
+						ButtonAlignment = (Dialog.ButtonAlignments)styleRadioGroup.SelectedItem
+					};
+
 					var add = new Button ("Add a button") {
 						X = Pos.Center (),
 						Y = Pos.Center ()
 					};
 					add.Clicked += () => {
 						var buttonId = buttons.Count;
-						//var button = new Button (btnText [buttonId % 10],
-						//	is_default: buttonId == 0);
-						var button = new Button (NumberToWords.Convert (buttonId),
-							is_default: buttonId == 0);
+						Button button;
+						if (glyphsNotWords.Checked) {
+							button = new Button (NumberToWords.Convert (buttonId) + " " + Char.ConvertFromUtf32 (buttonId + CODE_POINT),
+								is_default: buttonId == 0);
+						} else {
+							button = new Button (NumberToWords.Convert (buttonId),
+								is_default: buttonId == 0);
+						}
 						button.Clicked += () => {
 							clicked = buttonId;
 							Application.RequestStop ();

+ 2 - 2
UnitTests/ConsoleDriverTests.cs

@@ -576,7 +576,7 @@ namespace Terminal.Gui.ConsoleDrivers {
 ││  Hello World  │ │
 ││               │ │
 ││               │ │
-││     [ Ok ]    │ │
+││    [ Ok ]     │ │
 │└───────────────┘ │
 └──────────────────┘
 ";
@@ -593,7 +593,7 @@ namespace Terminal.Gui.ConsoleDrivers {
 ││  Hello World  │ │
 ││               │ │
 ││               │ │
-││     [ Ok ]    │ │
+││    [ Ok ]     │ │
 │└───────────────┘ │
 └──────────────────┘
 ";

+ 438 - 0
UnitTests/DialogTests.cs

@@ -0,0 +1,438 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using Terminal.Gui;
+using Xunit;
+using System.Globalization;
+using Xunit.Abstractions;
+using NStack;
+
+namespace Terminal.Gui.Views {
+
+	public class DialogTests {
+		readonly ITestOutputHelper output;
+
+		public DialogTests (ITestOutputHelper output)
+		{
+			this.output = output;
+		}
+
+		private Application.RunState RunButtonTestDialog (string title, int width, Dialog.ButtonAlignments align, params Button [] btns)
+		{
+			var dlg = new Dialog (title, width, 3, btns) { ButtonAlignment = align };
+			return Application.Begin (dlg);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_One ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			// E.g "|[ ok ]|"
+			var btnText = "ok";
+			var buttonRow = $"{d.VLine}   {d.LeftBracket} {btnText} {d.RightBracket}   {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+
+			d.SetBufferSize (width, 3);
+
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			buttonRow = $"{d.VLine}      {d.LeftBracket} {btnText} {d.RightBracket}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btnText));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			buttonRow = $"{d.VLine}      {d.LeftBracket} {btnText} {d.RightBracket}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btnText));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			buttonRow = $"{d.VLine}{d.LeftBracket} {btnText} {d.RightBracket}      {d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btnText));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_Two ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			// E.g "|[ yes ][ no ]|"
+			var btn1Text = "yes";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "no";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+
+			var buttonRow = $@"{d.VLine} {btn1} {btn2} {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+
+			d.SetBufferSize (buttonRow.Length, 3);
+
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			buttonRow = $@"{d.VLine}{btn1}   {btn2}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			buttonRow = $@"{d.VLine}  {btn1} {btn2}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			buttonRow = $@"{d.VLine}{btn1} {btn2}  {d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_Three ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			// E.g "|[ yes ][ no ][ maybe ]|"
+			var btn1Text = "yes";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "no";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+			var btn3Text = "maybe";
+			var btn3 = $"{d.LeftBracket} {btn3Text} {d.RightBracket}";
+
+			var buttonRow = $@"{d.VLine} {btn1} {btn2} {btn3} {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+
+			d.SetBufferSize (buttonRow.Length, 3);
+
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			buttonRow = $@"{d.VLine}{btn1}  {btn2}  {btn3}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			buttonRow = $@"{d.VLine}  {btn1} {btn2} {btn3}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			buttonRow = $@"{d.VLine}{btn1} {btn2} {btn3}  {d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_Four ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+
+			// E.g "|[ yes ][ no ][ maybe ]|"
+			var btn1Text = "yes";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "no";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+			var btn3Text = "maybe";
+			var btn3 = $"{d.LeftBracket} {btn3Text} {d.RightBracket}";
+			var btn4Text = "never";
+			var btn4 = $"{d.LeftBracket} {btn4Text} {d.RightBracket}";
+
+			var buttonRow = $"{d.VLine} {btn1} {btn2} {btn3} {btn4} {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+			d.SetBufferSize (buttonRow.Length, 3);
+
+			// Default - Center
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			buttonRow = $"{d.VLine}{btn1} {btn2}  {btn3}  {btn4}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			buttonRow = $"{d.VLine}  {btn1} {btn2} {btn3} {btn4}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}  {d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_Four_Wider ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+
+			// E.g "|[ yes ][ no ][ maybe ]|"
+			var btn1Text = "yes";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "no";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+			var btn3Text = "你你你你你"; // This is a wide char
+			var btn3 = $"{d.LeftBracket} {btn3Text} {d.RightBracket}";
+			// Requires a Nerd Font
+			var btn4Text = "\uE36E\uE36F\uE370\uE371\uE372\uE373";
+			var btn4 = $"{d.LeftBracket} {btn4Text} {d.RightBracket}";
+
+			// Note extra spaces to make dialog even wider
+			//                         12345                           123456
+			var buttonRow = $"{d.VLine}     {btn1} {btn2} {btn3} {btn4}      {d.VLine}";
+			var width = ustring.Make (buttonRow).ConsoleWidth;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+			d.SetBufferSize (width, 3);
+
+			// Default - Center
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			buttonRow = $"{d.VLine}{btn1}    {btn2}     {btn3}     {btn4}{d.VLine}";
+			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			buttonRow = $"{d.VLine}           {btn1} {btn2} {btn3} {btn4}{d.VLine}";
+			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}           {d.VLine}";
+			Assert.Equal (width, ustring.Make (buttonRow).ConsoleWidth);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void ButtonAlignment_Four_WideOdd ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+
+			// E.g "|[ yes ][ no ][ maybe ]|"
+			var btn1Text = "really long button 1";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "really long button 2";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+			var btn3Text = "really long button 3";
+			var btn3 = $"{d.LeftBracket} {btn3Text} {d.RightBracket}";
+			var btn4Text = "really long button 44"; // 44 is intentional to make length different than rest
+			var btn4 = $"{d.LeftBracket} {btn4Text} {d.RightBracket}";
+
+			// Note extra spaces to make dialog even wider
+			//                         12345                          123456
+			var buttonRow = $"{d.VLine}     {btn1} {btn2} {btn3} {btn4}      {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+			d.SetBufferSize (buttonRow.Length, 3);
+
+			// Default - Center
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			buttonRow = $"{d.VLine}{btn1}    {btn2}     {btn3}     {btn4}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			buttonRow = $"{d.VLine}           {btn1} {btn2} {btn3} {btn4}{d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			buttonRow = $"{d.VLine}{btn1} {btn2} {btn3} {btn4}           {d.VLine}";
+			Assert.Equal (width, buttonRow.Length);
+			runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, new Button (btn1Text), new Button (btn2Text), new Button (btn3Text), new Button (btn4Text));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Zero_Buttons_Works ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+
+			var buttonRow = $"{d.VLine}        {d.VLine}";
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+			d.SetBufferSize (buttonRow.Length, 3);
+
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, null);
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void One_Button_Works ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			var btnText = "ok";
+			var buttonRow = $"{d.VLine}   {d.LeftBracket} {btnText} {d.RightBracket}   {d.VLine}";
+
+			var width = buttonRow.Length;
+			var topRow = $"┌ {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}┐";
+			var bottomRow = $"└{new String (d.HLine.ToString () [0], width - 2)}┘";
+			d.SetBufferSize (buttonRow.Length, 3);
+
+			var runstate = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, new Button (btnText));
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+
+		[Fact]
+		[AutoInitShutdown]
+		public void Add_Button_Works ()
+		{
+			var d = ((FakeDriver)Application.Driver);
+
+			var title = "1234";
+			var btn1Text = "yes";
+			var btn1 = $"{d.LeftBracket} {btn1Text} {d.RightBracket}";
+			var btn2Text = "no";
+			var btn2 = $"{d.LeftBracket} {btn2Text} {d.RightBracket}";
+
+			// We test with one button first, but do this to get the width right for 2
+			var width = $@"{d.VLine} {btn1} {btn2} {d.VLine}".Length;
+			d.SetBufferSize (width, 3);
+
+			var topRow = $"{d.ULCorner} {title} {new String (d.HLine.ToString () [0], width - title.Length - 4)}{d.URCorner}";
+			var bottomRow = $"{d.LLCorner}{new String (d.HLine.ToString () [0], width - 2)}{d.LRCorner}";
+
+			// Default (center)
+			var dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Center };
+			var runstate = Application.Begin (dlg);
+			var buttonRow = $"{d.VLine}    {btn1}     {d.VLine}";
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+
+			// Now add a second button
+			buttonRow = $"{d.VLine} {btn1} {btn2} {d.VLine}";
+			dlg.AddButton (new Button (btn2Text));
+			bool first = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref first);
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Justify
+			dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Justify };
+			runstate = Application.Begin (dlg);
+			buttonRow = $"{d.VLine}         {btn1}{d.VLine}";
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+
+			// Now add a second button
+			buttonRow = $"{d.VLine}{btn1}   {btn2}{d.VLine}";
+			dlg.AddButton (new Button (btn2Text));
+			first = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref first);
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Right
+			dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Right };
+			runstate = Application.Begin (dlg);
+			buttonRow = $"{d.VLine}{new String (' ', width - btn1.Length - 2)}{btn1}{d.VLine}";
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+
+			// Now add a second button
+			buttonRow = $"{d.VLine}  {btn1} {btn2}{d.VLine}";
+			dlg.AddButton (new Button (btn2Text));
+			first = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref first);
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+
+			// Left
+			dlg = new Dialog (title, width, 3, new Button (btn1Text)) { ButtonAlignment = Dialog.ButtonAlignments.Left };
+			runstate = Application.Begin (dlg);
+			buttonRow = $"{d.VLine}{btn1}{new String (' ', width - btn1.Length - 2)}{d.VLine}";
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+
+			// Now add a second button
+			buttonRow = $"{d.VLine}{btn1} {btn2}  {d.VLine}";
+			dlg.AddButton (new Button (btn2Text));
+			first = false;
+			Application.RunMainLoopIteration (ref runstate, true, ref first);
+			GraphViewTests.AssertDriverContentsWithFrameAre ($"{topRow}\n{buttonRow}\n{bottomRow}", output);
+			Application.End (runstate);
+		}
+	}
+}