Преглед на файлове

Fixes #16 - Add ListTableSource for columned lists (#2603)

* Add ListTableSource for TableView to display an IList in a columned view

Also adds a global MinCellWidth to TableView

* Code style fixes

* Include padding in MinCellWidth calculation

* Unit Tests for Min/MaxCellWidth and ListTableSource

* Rename Redraw to Draw after refactor

* Rename Redraw to Draw after refactor

---------

Co-authored-by: Tig <[email protected]>
Nutzzz преди 2 години
родител
ревизия
5d1fe43362
променени са 4 файла, в които са добавени 675 реда и са изтрити 2 реда
  1. 199 0
      Terminal.Gui/Views/TableView/ListTableSource.cs
  2. 15 2
      Terminal.Gui/Views/TableView/TableView.cs
  3. 334 0
      UICatalog/Scenarios/ListColumns.cs
  4. 127 0
      UnitTests/Views/TableViewTests.cs

+ 199 - 0
Terminal.Gui/Views/TableView/ListTableSource.cs

@@ -0,0 +1,199 @@
+using NStack;
+using System;
+using System.Collections;
+using System.Data;
+using System.Linq;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// <see cref="ITableSource"/> implementation that wraps 
+	/// a <see cref="System.Collections.IList"/>.  This class is
+	/// mutable: changes are permitted to the wrapped <see cref="IList"/>.
+	/// </summary>
+	public class ListTableSource : ITableSource {
+		/// <summary>
+		/// The list this source wraps.
+		/// </summary>
+		public IList List;
+
+		/// <summary>
+		/// The style this source uses.
+		/// </summary>
+		public ListColumnStyle Style;
+
+		/// <summary>
+		/// The data table this source wraps.
+		/// </summary>
+		public DataTable DataTable { get; private set; }
+
+		private TableView _tableView;
+
+		private Rect _lastBounds;
+		private int _lastMaxCellWidth;
+		private int _lastMinCellWidth;
+		private ListColumnStyle _lastStyle;
+		private IList _lastList;
+
+		/// <summary>
+		/// Creates a new columned list table instance based on the data in <paramref name="list"/>
+		/// and dimensions from <paramref name="tableView"/>.
+		/// </summary>
+		/// <param name="list"></param>
+		/// <param name="tableView"></param>
+		/// <param name="style"></param>
+		public ListTableSource (IList list, TableView tableView, ListColumnStyle style)
+		{
+			this.List = list;
+			this._tableView = tableView;
+			Style = style;
+
+			this.DataTable = CreateTable (CalculateColumns ());
+
+			// TODO: Determine the best event for this
+			tableView.DrawContent += TableView_DrawContent;
+		}
+
+		/// <inheritdoc/>
+		public ListTableSource (IList list, TableView tableView) : this (list, tableView, new ListColumnStyle ()) { }
+
+		private void TableView_DrawContent (object sender, DrawEventArgs e)
+		{
+			if ((!_tableView.Bounds.Equals (_lastBounds)) ||
+				_tableView.MaxCellWidth != _lastMaxCellWidth ||
+				_tableView.MinCellWidth != _lastMinCellWidth ||
+				Style != _lastStyle ||
+				this.List != _lastList) {
+
+				this.DataTable = CreateTable (CalculateColumns ());
+			}
+			_lastBounds = _tableView.Bounds;
+			_lastMinCellWidth = _tableView.MaxCellWidth;
+			_lastMaxCellWidth = _tableView.MaxCellWidth;
+			_lastStyle = Style;
+			_lastList = this.List;
+		}
+
+		/// <inheritdoc/>
+		public object this [int row, int col] {
+			get {
+				int idx;
+				if (Style.Orientation == Orientation.Vertical) {
+					idx = (col * Rows) + row;
+				} else {
+					idx = (row * Columns) + col;
+				}
+				if (idx < 0 || idx >= Count) {
+					return null;
+				}
+				return this.List [idx];
+			}
+		}
+
+		/// <summary>
+		/// The number of items in the IList source
+		/// </summary>
+		public int Count => this.List.Count;
+
+		/// <inheritdoc/>
+		public int Rows => this.DataTable.Rows.Count;
+
+		/// <inheritdoc/>
+		public int Columns => this.DataTable.Columns.Count;
+
+		/// <inheritdoc/>
+		public string [] ColumnNames => Enumerable.Range (0, Columns).Select (n => n.ToString ()).ToArray ();
+
+		/// <summary>
+		/// Creates a DataTable from an IList to display in a <see cref="TableView"/>
+		/// </summary>
+		private DataTable CreateTable (int cols = 1)
+		{
+			var table = new DataTable ();
+			for (int col = 0; col < cols; col++) {
+				table.Columns.Add (new DataColumn (col.ToString ()));
+			}
+			for (int row = 0; row < (Count / table.Columns.Count); row++) {
+				table.Rows.Add ();
+			}
+			// return partial row
+			if (Count % table.Columns.Count != 0) {
+				table.Rows.Add ();
+			}
+
+			return table;
+		}
+
+		/// <summary>
+		/// Returns the size in characters of the longest value read from <see cref="ListTableSource.List"/>
+		/// </summary>
+		/// <returns></returns>
+		private int CalculateMaxLength ()
+		{
+			if (List == null || Count == 0) {
+				return 0;
+			}
+
+			int maxLength = 0;
+			foreach (var t in List) {
+				int l;
+				if (t is ustring u) {
+					l = TextFormatter.GetTextWidth (u);
+				} else if (t is string s) {
+					l = s.Length;
+				} else {
+					l = t.ToString ().Length;
+				}
+
+				if (l > maxLength) {
+					maxLength = l;
+				}
+			}
+
+			return maxLength;
+		}
+
+		private int CalculateColumns ()
+		{
+			int cols;
+
+			int colWidth = CalculateMaxLength ();
+			if (colWidth > _tableView.MaxCellWidth) {
+				colWidth = _tableView.MaxCellWidth;
+			}
+
+			if (_tableView.MinCellWidth > 0 && colWidth < _tableView.MinCellWidth) {
+				if (_tableView.MinCellWidth > _tableView.MaxCellWidth) {
+					colWidth = _tableView.MaxCellWidth;
+				} else {
+					colWidth = _tableView.MinCellWidth;
+				}
+			}
+			if ((Style.Orientation == Orientation.Vertical) != Style.ScrollParallel) {
+				float f = (float)_tableView.Bounds.Height - _tableView.GetHeaderHeight ();
+				cols = (int)Math.Ceiling (Count / f);
+			} else {
+				cols = ((int)Math.Ceiling (((float)_tableView.Bounds.Width - 1) / colWidth)) - 2;
+			}
+
+			return (cols > 1) ? cols : 1;
+		}
+
+		/// <summary>
+		/// Defines rendering options that affect how the view is displayed.
+		/// </summary>
+		public class ListColumnStyle {
+
+			/// <summary>
+			/// Gets or sets an Orientation enum indicating whether to populate data down each column
+			/// rather than across each row.  Defaults to <see cref="Orientation.Horizontal"/>.
+			/// </summary>
+			public Orientation Orientation { get; set; } = Orientation.Horizontal;
+
+			/// <summary>
+			/// Gets or sets a flag indicating whether to scroll in the same direction as <see cref="ListTableSource.ListColumnStyle.Orientation"/>.
+			/// Defaults to <see langword="false"/>.
+			/// </summary>
+			public bool ScrollParallel { get; set; } = false;
+		}
+	}
+}

+ 15 - 2
Terminal.Gui/Views/TableView/TableView.cs

@@ -121,6 +121,11 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// The minimum number of characters to render in any given column.
+		/// </summary>
+		public int MinCellWidth { get; set; }
+
 		/// <summary>
 		/// The maximum number of characters to render in any given column.  This prevents one long column from pushing out all the others
 		/// </summary>
@@ -329,7 +334,7 @@ namespace Terminal.Gui {
 		/// Returns the amount of vertical space currently occupied by the header or 0 if it is not visible.
 		/// </summary>
 		/// <returns></returns>
-		private int GetHeaderHeightIfAny ()
+		internal int GetHeaderHeightIfAny ()
 		{
 			return ShouldRenderHeaders () ? GetHeaderHeight () : 0;
 		}
@@ -338,7 +343,7 @@ namespace Terminal.Gui {
 		/// Returns the amount of vertical space required to display the header
 		/// </summary>
 		/// <returns></returns>
-		private int GetHeaderHeight ()
+		internal int GetHeaderHeight ()
 		{
 			int heightRequired = Style.ShowHeaders ? 1 : 0;
 
@@ -1619,6 +1624,14 @@ namespace Terminal.Gui {
 				// is there enough space for this column (and it's data)?
 				colWidth = CalculateMaxCellWidth (col, rowsToRender, colStyle) + padding;
 
+				if (MinCellWidth > 0 && colWidth < (MinCellWidth + padding)) {
+					if (MinCellWidth > MaxCellWidth) {
+						colWidth = MaxCellWidth + padding;
+					} else {
+						colWidth = MinCellWidth + padding;
+					}
+				}
+
 				// there is not enough space for this columns 
 				// visible content
 				if (usedSpace + colWidth > availableHorizontalSpace) {

+ 334 - 0
UICatalog/Scenarios/ListColumns.cs

@@ -0,0 +1,334 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Data;
+using Terminal.Gui;
+using static Terminal.Gui.TableView;
+
+namespace UICatalog.Scenarios {
+
+	[ScenarioMetadata (Name: "ListColumns", Description: "Implements a columned list via a data table.")]
+	[ScenarioCategory ("TableView")]
+	[ScenarioCategory ("Controls")]
+	[ScenarioCategory ("Dialogs")]
+	[ScenarioCategory ("Text and Formatting")]
+	[ScenarioCategory ("Top Level Windows")]
+	public class ListColumns : Scenario {
+		TableView listColView;
+		DataTable currentTable;
+		private MenuItem _miCellLines;
+		private MenuItem _miExpandLastColumn;
+		private MenuItem _miAlwaysUseNormalColorForVerticalCellLines;
+		private MenuItem _miSmoothScrolling;
+		private MenuItem _miAlternatingColors;
+		private MenuItem _miCursor;
+		private MenuItem _miTopline;
+		private MenuItem _miBottomline;
+		private MenuItem _miOrientVertical;
+		private MenuItem _miScrollParallel;
+
+		ColorScheme alternatingColorScheme;
+
+		public override void Setup ()
+		{
+			Win.Title = this.GetName ();
+			Win.Y = 1; // menu
+			Win.Height = Dim.Fill (1); // status bar
+			Application.Top.LayoutSubviews ();
+
+			this.listColView = new TableView () {
+				X = 0,
+				Y = 0,
+				Width = Dim.Fill (),
+				Height = Dim.Fill (1),
+				Style = new TableStyle {
+					ShowHeaders = false,
+					ShowHorizontalHeaderOverline = false,
+					ShowHorizontalHeaderUnderline = false,
+					ShowHorizontalBottomline = false,
+					ExpandLastColumn = false,
+				}
+			};
+			var listColStyle = new ListTableSource.ListColumnStyle ();
+
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("_File", new MenuItem [] {
+					new MenuItem ("Open_BigListExample", "", () => OpenSimpleList (true)),
+					new MenuItem ("Open_SmListExample", "", () => OpenSimpleList (false)),
+					new MenuItem ("_CloseExample", "", () => CloseExample ()),
+					new MenuItem ("_Quit", "", () => Quit()),
+				}),
+				new MenuBarItem ("_View", new MenuItem [] {
+					_miTopline = new MenuItem ("_TopLine", "", () => ToggleTopline ()) { Checked = listColView.Style.ShowHorizontalHeaderOverline, CheckType = MenuItemCheckStyle.Checked },
+					_miBottomline = new MenuItem ("_BottomLine", "", () => ToggleBottomline ()) { Checked = listColView.Style.ShowHorizontalBottomline, CheckType = MenuItemCheckStyle.Checked },
+					_miCellLines = new MenuItem ("_CellLines", "", () => ToggleCellLines ()) { Checked = listColView.Style.ShowVerticalCellLines, CheckType = MenuItemCheckStyle.Checked },
+					_miExpandLastColumn = new MenuItem ("_ExpandLastColumn", "", () => ToggleExpandLastColumn ()) { Checked = listColView.Style.ExpandLastColumn, CheckType = MenuItemCheckStyle.Checked },
+					_miAlwaysUseNormalColorForVerticalCellLines = new MenuItem ("_AlwaysUseNormalColorForVerticalCellLines", "", () => ToggleAlwaysUseNormalColorForVerticalCellLines ()) { Checked = listColView.Style.AlwaysUseNormalColorForVerticalCellLines, CheckType = MenuItemCheckStyle.Checked },
+					_miSmoothScrolling = new MenuItem ("_SmoothHorizontalScrolling", "", () => ToggleSmoothScrolling ()) { Checked = listColView.Style.SmoothHorizontalScrolling, CheckType = MenuItemCheckStyle.Checked },
+					_miAlternatingColors = new MenuItem ("Alternating Colors", "", () => ToggleAlternatingColors ()) { CheckType = MenuItemCheckStyle.Checked},
+					_miCursor = new MenuItem ("Invert Selected Cell First Character", "", () => ToggleInvertSelectedCellFirstCharacter ()) { Checked = listColView.Style.InvertSelectedCellFirstCharacter,CheckType = MenuItemCheckStyle.Checked},
+				}),
+				new MenuBarItem ("_List", new MenuItem [] {
+					//new MenuItem ("_Hide Headers", "", HideHeaders),
+					_miOrientVertical = new MenuItem ("_OrientVertical", "", () => ToggleVerticalOrientation ()) { Checked = listColStyle.Orientation == Orientation.Vertical, CheckType = MenuItemCheckStyle.Checked },
+					_miScrollParallel = new MenuItem ("_ScrollParallel", "", () => ToggleScrollParallel ()) { Checked = listColStyle.ScrollParallel, CheckType = MenuItemCheckStyle.Checked },
+					new MenuItem ("Set _Max Cell Width", "", SetListMaxWidth),
+					new MenuItem ("Set Mi_n Cell Width", "", SetListMinWidth),
+				}),
+			});
+
+			Application.Top.Add (menu);
+
+			var statusBar = new StatusBar (new StatusItem [] {
+				new StatusItem(Key.F2, "~F2~ OpenBigListEx", () => OpenSimpleList (true)),
+				new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample ()),
+				new StatusItem(Key.F4, "~F4~ OpenSmListEx", () => OpenSimpleList (false)),
+				new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()),
+			});
+			Application.Top.Add (statusBar);
+
+			Win.Add (listColView);
+
+			var selectedCellLabel = new Label () {
+				X = 0,
+				Y = Pos.Bottom (listColView),
+				Text = "0,0",
+				Width = Dim.Fill (),
+				TextAlignment = TextAlignment.Right
+
+			};
+
+			Win.Add (selectedCellLabel);
+
+			listColView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{listColView.SelectedRow},{listColView.SelectedColumn}"; };
+			listColView.KeyPress += TableViewKeyPress;
+
+			SetupScrollBar ();
+
+			alternatingColorScheme = new ColorScheme () {
+
+				Disabled = Win.ColorScheme.Disabled,
+				HotFocus = Win.ColorScheme.HotFocus,
+				Focus = Win.ColorScheme.Focus,
+				Normal = Application.Driver.MakeAttribute (Color.White, Color.BrightBlue)
+			};
+
+			// if user clicks the mouse in TableView
+			listColView.MouseClick += (s, e) => {
+
+				listColView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol);
+			};
+
+			listColView.AddKeyBinding (Key.Space, Command.ToggleChecked);
+		}
+
+		private void SetupScrollBar ()
+		{
+			var scrollBar = new ScrollBarView (listColView, true); // (listColView, true, true);
+
+			scrollBar.ChangedPosition += (s, e) => {
+				listColView.RowOffset = scrollBar.Position;
+				if (listColView.RowOffset != scrollBar.Position) {
+					scrollBar.Position = listColView.RowOffset;
+				}
+				listColView.SetNeedsDisplay ();
+			};
+			/*
+			scrollBar.OtherScrollBarView.ChangedPosition += (s,e) => {
+				listColView.ColumnOffset = scrollBar.OtherScrollBarView.Position;
+				if (listColView.ColumnOffset != scrollBar.OtherScrollBarView.Position) {
+					scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset;
+				}
+				listColView.SetNeedsDisplay ();
+			};
+			*/
+
+			listColView.DrawContent += (s, e) => {
+				scrollBar.Size = listColView.Table?.Rows ?? 0;
+				scrollBar.Position = listColView.RowOffset;
+				//scrollBar.OtherScrollBarView.Size = listColView.Table?.Columns - 1 ?? 0;
+				//scrollBar.OtherScrollBarView.Position = listColView.ColumnOffset;
+				scrollBar.Refresh ();
+			};
+
+		}
+
+		private void TableViewKeyPress (object sender, KeyEventEventArgs e)
+		{
+			if (e.KeyEvent.Key == Key.DeleteChar) {
+
+				// set all selected cells to null
+				foreach (var pt in listColView.GetAllSelectedCells ()) {
+					currentTable.Rows [pt.Y] [pt.X] = DBNull.Value;
+				}
+
+				listColView.Update ();
+				e.Handled = true;
+			}
+
+		}
+
+		private void ToggleTopline ()
+		{
+			_miTopline.Checked = !_miTopline.Checked;
+			listColView.Style.ShowHorizontalHeaderOverline = (bool)_miTopline.Checked;
+			listColView.Update ();
+		}
+		private void ToggleBottomline ()
+		{
+			_miBottomline.Checked = !_miBottomline.Checked;
+			listColView.Style.ShowHorizontalBottomline = (bool)_miBottomline.Checked;
+			listColView.Update ();
+		}
+		private void ToggleExpandLastColumn ()
+		{
+			_miExpandLastColumn.Checked = !_miExpandLastColumn.Checked;
+			listColView.Style.ExpandLastColumn = (bool)_miExpandLastColumn.Checked;
+
+			listColView.Update ();
+
+		}
+
+		private void ToggleAlwaysUseNormalColorForVerticalCellLines ()
+		{
+			_miAlwaysUseNormalColorForVerticalCellLines.Checked = !_miAlwaysUseNormalColorForVerticalCellLines.Checked;
+			listColView.Style.AlwaysUseNormalColorForVerticalCellLines = (bool)_miAlwaysUseNormalColorForVerticalCellLines.Checked;
+
+			listColView.Update ();
+		}
+		private void ToggleSmoothScrolling ()
+		{
+			_miSmoothScrolling.Checked = !_miSmoothScrolling.Checked;
+			listColView.Style.SmoothHorizontalScrolling = (bool)_miSmoothScrolling.Checked;
+
+			listColView.Update ();
+
+		}
+		private void ToggleCellLines ()
+		{
+			_miCellLines.Checked = !_miCellLines.Checked;
+			listColView.Style.ShowVerticalCellLines = (bool)_miCellLines.Checked;
+			listColView.Update ();
+		}
+		private void ToggleAlternatingColors ()
+		{
+			//toggle menu item
+			_miAlternatingColors.Checked = !_miAlternatingColors.Checked;
+
+			if (_miAlternatingColors.Checked == true) {
+				listColView.Style.RowColorGetter = (a) => { return a.RowIndex % 2 == 0 ? alternatingColorScheme : null; };
+			} else {
+				listColView.Style.RowColorGetter = null;
+			}
+			listColView.SetNeedsDisplay ();
+		}
+
+		private void ToggleInvertSelectedCellFirstCharacter ()
+		{
+			//toggle menu item
+			_miCursor.Checked = !_miCursor.Checked;
+			listColView.Style.InvertSelectedCellFirstCharacter = (bool)_miCursor.Checked;
+			listColView.SetNeedsDisplay ();
+		}
+
+		private void ToggleVerticalOrientation ()
+		{
+			_miOrientVertical.Checked = !_miOrientVertical.Checked;
+			if ((ListTableSource)listColView.Table != null) {
+				((ListTableSource)listColView.Table).Style.Orientation = (bool)_miOrientVertical.Checked ? Orientation.Vertical : Orientation.Horizontal;
+				listColView.SetNeedsDisplay ();
+			}
+		}
+
+		private void ToggleScrollParallel ()
+		{
+			_miScrollParallel.Checked = !_miScrollParallel.Checked;
+			if ((ListTableSource)listColView.Table != null) {
+				((ListTableSource)listColView.Table).Style.ScrollParallel = (bool)_miScrollParallel.Checked;
+				listColView.SetNeedsDisplay ();
+			}
+		}
+
+		private void SetListMinWidth ()
+		{
+			RunListWidthDialog ("MinCellWidth", (s, v) => s.MinCellWidth = v, (s) => s.MinCellWidth);
+			listColView.SetNeedsDisplay ();
+		}
+
+		private void SetListMaxWidth ()
+		{
+			RunListWidthDialog ("MaxCellWidth", (s, v) => s.MaxCellWidth = v, (s) => s.MaxCellWidth);
+			listColView.SetNeedsDisplay ();
+		}
+
+		private void RunListWidthDialog (string prompt, Action<TableView, int> setter, Func<TableView, int> getter)
+		{
+			var accepted = false;
+			var ok = new Button ("Ok", is_default: true);
+			ok.Clicked += (s, e) => { accepted = true; Application.RequestStop (); };
+			var cancel = new Button ("Cancel");
+			cancel.Clicked += (s, e) => { Application.RequestStop (); };
+			var d = new Dialog (ok, cancel) { Title = prompt };
+
+			var tf = new TextField () {
+				Text = getter (listColView).ToString (),
+				X = 0,
+				Y = 1,
+				Width = Dim.Fill ()
+			};
+
+			d.Add (tf);
+			tf.SetFocus ();
+
+			Application.Run (d);
+
+			if (accepted) {
+
+				try {
+					setter (listColView, int.Parse (tf.Text.ToString ()));
+				} catch (Exception ex) {
+					MessageBox.ErrorQuery (60, 20, "Failed to set", ex.Message, "Ok");
+				}
+			}
+		}
+
+		private void CloseExample ()
+		{
+			listColView.Table = null;
+		}
+
+		private void Quit ()
+		{
+			Application.RequestStop ();
+		}
+
+		private void OpenSimpleList (bool big)
+		{
+			SetTable (BuildSimpleList (big ? 1023 : 31));
+		}
+
+		private void SetTable (IList list)
+		{
+			listColView.Table = new ListTableSource (list, listColView);
+			if ((ListTableSource)listColView.Table != null) {
+				currentTable = ((ListTableSource)listColView.Table).DataTable;
+			}
+		}
+
+		/// <summary>
+		/// Builds a simple list in which values are the index.  This helps testing that scrolling etc is working correctly and not skipping out values when paging
+		/// </summary>
+		/// <param name="items"></param>
+		/// <returns></returns>
+		public static IList BuildSimpleList (int items)
+		{
+			var list = new List<object> ();
+
+			for (int i = 0; i < items; i++) {
+				list.Add ("Item " + i);
+			}
+
+			return list;
+		}
+	}
+}

+ 127 - 0
UnitTests/Views/TableViewTests.cs

@@ -9,6 +9,8 @@ using System.Globalization;
 using Xunit.Abstractions;
 using System.Reflection;
 using Terminal.Gui.ViewTests;
+using System.Collections;
+using static Terminal.Gui.SpinnerStyle;
 
 namespace Terminal.Gui.ViewsTests {
 
@@ -1930,6 +1932,42 @@ namespace Terminal.Gui.ViewsTests {
 ";
 			TestHelpers.AssertDriverContentsAre (expected, output);
 
+			tableView.Bounds = new Rect (0, 0, 25, 5);
+
+			// revert style change
+			style.MinAcceptableWidth = TableView.DefaultMinAcceptableWidth;
+
+			// Now let's test the global MaxCellWidth and MinCellWidth
+			tableView.Style.ExpandLastColumn = false;
+			tableView.MaxCellWidth = 10;
+			tableView.MinCellWidth = 3;
+
+			tableView.LayoutSubviews ();
+			tableView.Draw ();
+			expected =
+@"
+│A  │B  │Very Long │    │
+├───┼───┼──────────┼────┤
+│1  │2  │aaaaaaaaaa│    │
+│1  │2  │aaa       │    │
+";
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			// MaxCellWidth limits MinCellWidth
+			tableView.MaxCellWidth = 5;
+			tableView.MinCellWidth = 10;
+
+			tableView.LayoutSubviews ();
+			tableView.Draw ();
+			expected =
+@"
+│A    │B    │Very │     │
+├─────┼─────┼─────┼─────┤
+│1    │2    │aaaaa│     │
+│1    │2    │aaa  │     │
+";
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
 			Application.Shutdown ();
 		}
 
@@ -2489,6 +2527,95 @@ A B C
 			Assert.Null (col);
 		}
 
+		/// <summary>
+		/// Builds a simple list with the requested number of string items
+		/// </summary>
+		/// <param name="items"></param>
+		/// <returns></returns>
+		public static IList BuildList (int items)
+		{
+			var list = new List<string> ();
+			for (int i = 0; i < items; i++) {
+				list.Add ("Item " + i);
+			}
+			return list.ToArray ();
+		}
+
+		[Theory, AutoInitShutdown]
+		[InlineData (new object [] { Orientation.Horizontal, false })]
+		[InlineData (new object [] { Orientation.Vertical, false })]
+		[InlineData (new object [] { Orientation.Horizontal, true })]
+		[InlineData (new object [] { Orientation.Vertical, true })]
+		public void TestListTableSource (Orientation orient, bool parallel)
+		{
+			var list = BuildList (16);
+
+			var tv = new TableView ();
+			//tv.BeginInit (); tv.EndInit ();
+			tv.ColorScheme = Colors.TopLevel;
+			tv.Bounds = new Rect (0, 0, 25, 4);
+			tv.Style = new () {
+				ShowHeaders = false,
+				ShowHorizontalHeaderOverline = false,
+				ShowHorizontalHeaderUnderline = false
+			};
+			var listStyle = new ListTableSource.ListColumnStyle () {
+				Orientation = orient,
+				ScrollParallel = parallel
+			};
+
+			tv.Table = new ListTableSource (list, tv, listStyle);
+
+			tv.LayoutSubviews ();
+
+			tv.Draw ();
+
+			string horizPerpExpected =
+				@"
+│Item 0│Item 1          │
+│Item 2│Item 3          │
+│Item 4│Item 5          │
+│Item 6│Item 7          │";
+
+			string horizParaExpected =
+				@"
+│Item 0 │Item 1 │Item 2 │
+│Item 4 │Item 5 │Item 6 │
+│Item 8 │Item 9 │Item 10│
+│Item 12│Item 13│Item 14│";
+
+			string vertPerpExpected =
+				@"
+│Item 0│Item 4│Item 8   │
+│Item 1│Item 5│Item 9   │
+│Item 2│Item 6│Item 10  │
+│Item 3│Item 7│Item 11  │";
+
+			string vertParaExpected =
+				@"
+│Item 0│Item 8          │
+│Item 1│Item 9          │
+│Item 2│Item 10         │
+│Item 3│Item 11         │";
+
+			string expected;
+			if (orient == Orientation.Vertical)
+				if (parallel) {
+					expected = vertParaExpected;
+				} else {
+					expected = vertPerpExpected;
+				}
+			else {
+				if (parallel) {
+					expected = horizParaExpected;
+				} else {
+					expected = horizPerpExpected;
+				}
+			}
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+		}
+
 		[Fact, AutoInitShutdown]
 		public void TestEnumerableDataSource_BasicTypes ()
 		{