Bläddra i källkod

Merge pull request #1081 from BDisp/scroll-bar-view

Fixes #1073, #1058, #480 #1049. Provides more automation to the ScrollBarView, allowing easily implement on any view.
Charlie Kindel 4 år sedan
förälder
incheckning
77add786fc

+ 1 - 1
Terminal.Gui/Core/Application.cs

@@ -351,7 +351,7 @@ namespace Terminal.Gui {
 					var ry = y - startFrame.Y;
 					for (int i = count - 1; i >= 0; i--) {
 						View v = start.InternalSubviews [i];
-						if (v.Frame.Contains (rx, ry)) {
+						if (v.Visible && v.Frame.Contains (rx, ry)) {
 							var deep = FindDeepestView (v, rx, ry, out resx, out resy);
 							if (deep == null)
 								return v;

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

@@ -1342,6 +1342,7 @@ namespace Terminal.Gui {
 							// Draw the subview
 							// Use the view's bounds (view-relative; Location will always be (0,0)
 							if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) {
+								view.OnDrawContent (view.Bounds);
 								view.Redraw (view.Bounds);
 							}
 						}

+ 155 - 30
Terminal.Gui/Views/ListView.cs

@@ -34,6 +34,11 @@ namespace Terminal.Gui {
 		/// </summary>
 		int Count { get; }
 
+		/// <summary>
+		/// Returns the maximum length of elements to display
+		/// </summary>
+		int Length { get; }
+
 		/// <summary>
 		/// This method is invoked to render a specified item, the method should cover the entire provided width.
 		/// </summary>
@@ -45,10 +50,11 @@ namespace Terminal.Gui {
 		/// <param name="col">The column where the rendering will start</param>
 		/// <param name="line">The line where the rendering will be done.</param>
 		/// <param name="width">The width that must be filled out.</param>
+		/// <param name="start">The index of the string to be displayed.</param>
 		/// <remarks>
 		///   The default color will be set before this method is invoked, and will be based on whether the item is selected or not.
 		/// </remarks>
-		void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width);
+		void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0);
 
 		/// <summary>
 		/// Should return whether the specified item is currently marked.
@@ -103,7 +109,7 @@ namespace Terminal.Gui {
 	/// </para>
 	/// </remarks>
 	public class ListView : View {
-		int top;
+		int top, left;
 		int selected;
 
 		IListDataSource source;
@@ -146,7 +152,7 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <value>An item implementing the IList interface.</value>
 		/// <remarks>
-		///  Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custome rendering.
+		///  Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom rendering.
 		/// </remarks>
 		public Task SetSourceAsync (IList source)
 		{
@@ -204,13 +210,35 @@ namespace Terminal.Gui {
 				if (source == null)
 					return;
 
-				if (top < 0 || top >= source.Count)
+				if (value < 0 || (source.Count > 0 && value >= source.Count))
 					throw new ArgumentException ("value");
 				top = value;
 				SetNeedsDisplay ();
 			}
 		}
 
+		/// <summary>
+		/// Gets or sets the left column where the item start to be displayed at on the <see cref="ListView"/>.
+		/// </summary>
+		/// <value>The left position.</value>
+		public int LeftItem {
+			get => left;
+			set {
+				if (source == null)
+					return;
+
+				if (value < 0 || (Maxlength > 0 && value >= Maxlength))
+					throw new ArgumentException ("value");
+				left = value;
+				SetNeedsDisplay ();
+			}
+		}
+
+		/// <summary>
+		/// Gets the widest item.
+		/// </summary>
+		public int Maxlength => (source?.Length) ?? 0;
+
 		/// <summary>
 		/// Gets or sets the index of the currently selected item.
 		/// </summary>
@@ -229,7 +257,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-
 		static IListDataSource MakeWrapper (IList source)
 		{
 			return new ListWrapper (source);
@@ -298,14 +325,10 @@ namespace Terminal.Gui {
 			Driver.SetAttribute (current);
 			Move (0, 0);
 			var f = Frame;
-			if (selected < top) {
-				top = selected;
-			} else if (selected >= top + f.Height) {
-				top = selected;
-			}
 			var item = top;
 			bool focused = HasFocus;
 			int col = allowsMarking ? 2 : 0;
+			int start = left;
 
 			for (int row = 0; row < f.Height; row++, item++) {
 				bool isSelected = item == selected;
@@ -325,7 +348,7 @@ namespace Terminal.Gui {
 						Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) : (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
 						Driver.AddRune (' ');
 					}
-					Source.Render (this, Driver, isSelected, item, col, row, f.Width - col);
+					Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
 				}
 			}
 		}
@@ -477,8 +500,11 @@ namespace Terminal.Gui {
 			} else if (selected + 1 < source.Count) { //can move by down by one.
 				selected++;
 
-				if (selected >= top + Frame.Height)
+				if (selected >= top + Frame.Height) {
 					top++;
+				} else if (selected < top) {
+					top = selected;
+				}
 				OnSelectedChanged ();
 				SetNeedsDisplay ();
 			} else if (selected == 0) {
@@ -511,8 +537,11 @@ namespace Terminal.Gui {
 				if (selected > Source.Count) {
 					selected = Source.Count - 1;
 				}
-				if (selected < top)
+				if (selected < top) {
 					top = selected;
+				} else if (selected > top + Frame.Height) {
+					top = Math.Max (selected - Frame.Height + 1, 0);
+				}
 				OnSelectedChanged ();
 				SetNeedsDisplay ();
 			}
@@ -551,6 +580,46 @@ namespace Terminal.Gui {
 			return true;
 		}
 
+		/// <summary>
+		/// Scrolls the view down.
+		/// </summary>
+		/// <param name="lines">Number of lines to scroll down.</param>
+		public virtual void ScrollDown (int lines)
+		{
+			top = Math.Max (Math.Min (top + lines, source.Count - 1), 0);
+			SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// Scrolls the view up.
+		/// </summary>
+		/// <param name="lines">Number of lines to scroll up.</param>
+		public virtual void ScrollUp (int lines)
+		{
+			top = Math.Max (top - lines, 0);
+			SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// Scrolls the view right.
+		/// </summary>
+		/// <param name="cols">Number of columns to scroll right.</param>
+		public virtual void ScrollRight (int cols)
+		{
+			left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0);
+			SetNeedsDisplay ();
+		}
+
+		/// <summary>
+		/// Scrolls the view left.
+		/// </summary>
+		/// <param name="cols">Number of columns to scroll left.</param>
+		public virtual void ScrollLeft (int cols)
+		{
+			left = Math.Max (left - cols, 0);
+			SetNeedsDisplay ();
+		}
+
 		int lastSelectedItem = -1;
 		private bool allowsMultipleSelection = true;
 
@@ -563,7 +632,9 @@ namespace Terminal.Gui {
 			if (selected != lastSelectedItem) {
 				var value = source?.Count > 0 ? source.ToList () [selected] : null;
 				SelectedItemChanged?.Invoke (new ListViewItemEventArgs (selected, value));
-				lastSelectedItem = selected;
+				if (HasFocus) {
+					lastSelectedItem = selected;
+				}
 				return true;
 			}
 
@@ -587,6 +658,7 @@ namespace Terminal.Gui {
 		public override bool OnEnter (View view)
 		{
 			if (lastSelectedItem == -1) {
+				EnsuresVisibilitySelectedItem ();
 				OnSelectedChanged ();
 				return true;
 			}
@@ -605,6 +677,16 @@ namespace Terminal.Gui {
 			return false;
 		}
 
+		void EnsuresVisibilitySelectedItem ()
+		{
+			SuperView?.LayoutSubviews ();
+			if (selected < top) {
+				top = selected;
+			} else if (Frame.Height > 0 && selected >= top + Frame.Height) {
+				top = Math.Max (selected - Frame.Height + 2, 0);
+			}
+		}
+
 		///<inheritdoc/>
 		public override void PositionCursor ()
 		{
@@ -618,7 +700,8 @@ namespace Terminal.Gui {
 		public override bool MouseEvent (MouseEvent me)
 		{
 			if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
-				me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp)
+				me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp &&
+				me.Flags != MouseFlags.WheeledRight && me.Flags != MouseFlags.WheeledLeft)
 				return false;
 
 			if (!HasFocus && CanFocus) {
@@ -630,10 +713,16 @@ namespace Terminal.Gui {
 			}
 
 			if (me.Flags == MouseFlags.WheeledDown) {
-				MoveDown ();
+				ScrollDown (1);
 				return true;
 			} else if (me.Flags == MouseFlags.WheeledUp) {
-				MoveUp ();
+				ScrollUp (1);
+				return true;
+			} else if (me.Flags == MouseFlags.WheeledRight) {
+				ScrollRight (1);
+				return true;
+			} else if (me.Flags == MouseFlags.WheeledLeft) {
+				ScrollLeft (1);
 				return true;
 			}
 
@@ -655,6 +744,8 @@ namespace Terminal.Gui {
 
 			return true;
 		}
+
+
 	}
 
 	/// <summary>
@@ -664,7 +755,7 @@ namespace Terminal.Gui {
 	public class ListWrapper : IListDataSource {
 		IList src;
 		BitArray marks;
-		int count;
+		int count, len;
 
 		/// <summary>
 		/// Initializes a new instance of <see cref="ListWrapper"/> given an <see cref="IList"/>
@@ -675,7 +766,8 @@ namespace Terminal.Gui {
 			if (source != null) {
 				count = source.Count;
 				marks = new BitArray (count);
-				this.src = source;
+				src = source;
+				len = GetMaxLengthItem ();
 			}
 		}
 
@@ -684,11 +776,42 @@ namespace Terminal.Gui {
 		/// </summary>
 		public int Count => src != null ? src.Count : 0;
 
-		void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width)
+		/// <summary>
+		/// Gets the maximum item length in the <see cref="IList"/>.
+		/// </summary>
+		public int Length => len;
+
+		int GetMaxLengthItem ()
+		{
+			if (src?.Count == 0) {
+				return 0;
+			}
+
+			int maxLength = 0;
+			for (int i = 0; i < src.Count; i++) {
+				var t = src [i];
+				int l;
+				if (t is ustring u) {
+					l = u.RuneCount;
+				} else if (t is string s) {
+					l = s.Length;
+				} else {
+					l = t.ToString ().Length;
+				}
+
+				if (l > maxLength) {
+					maxLength = l;
+				}
+			}
+
+			return maxLength;
+		}
+
+		void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
 		{
 			int byteLen = ustr.Length;
 			int used = 0;
-			for (int i = 0; i < byteLen;) {
+			for (int i = start; i < byteLen;) {
 				(var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
 				var count = Rune.ColumnWidth (rune);
 				if (used + count > width)
@@ -712,19 +835,21 @@ namespace Terminal.Gui {
 		/// <param name="col">The col where to move.</param>
 		/// <param name="line">The line where to move.</param>
 		/// <param name="width">The item width.</param>
-		public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width)
+		/// <param name="start">The index of the string to be displayed.</param>
+		public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0)
 		{
 			container.Move (col, line);
 			var t = src [item];
 			if (t == null) {
 				RenderUstr (driver, ustring.Make (""), col, line, width);
 			} else {
-				if (t is ustring) {
-					RenderUstr (driver, (ustring)t, col, line, width);
-				} else if (t is string) {
-					RenderUstr (driver, (string)t, col, line, width);
-				} else
-					RenderUstr (driver, t.ToString (), col, line, width);
+				if (t is ustring u) {
+					RenderUstr (driver, u, col, line, width, start);
+				} else if (t is string s) {
+					RenderUstr (driver, s, col, line, width, start);
+				} else {
+					RenderUstr (driver, t.ToString (), col, line, width, start);
+				}
 			}
 		}
 
@@ -770,14 +895,14 @@ namespace Terminal.Gui {
 		/// </summary>
 		public int Item { get; }
 		/// <summary>
-		/// The the <see cref="ListView"/> item.
+		/// The <see cref="ListView"/> item.
 		/// </summary>
 		public object Value { get; }
 
 		/// <summary>
 		/// Initializes a new instance of <see cref="ListViewItemEventArgs"/>
 		/// </summary>
-		/// <param name="item">The index of the the <see cref="ListView"/> item.</param>
+		/// <param name="item">The index of the <see cref="ListView"/> item.</param>
 		/// <param name="value">The <see cref="ListView"/> item</param>
 		public ListViewItemEventArgs (int item, object value)
 		{

+ 662 - 0
Terminal.Gui/Views/ScrollBarView.cs

@@ -0,0 +1,662 @@
+//
+// ScrollBarView.cs: ScrollBarView view.
+//
+// Authors:
+//   Miguel de Icaza ([email protected])
+//
+
+using System;
+
+namespace Terminal.Gui {
+	/// <summary>
+	/// ScrollBarViews are views that display a 1-character scrollbar, either horizontal or vertical
+	/// </summary>
+	/// <remarks>
+	/// <para>
+	///   The scrollbar is drawn to be a representation of the Size, assuming that the 
+	///   scroll position is set at Position.
+	/// </para>
+	/// <para>
+	///   If the region to display the scrollbar is larger than three characters, 
+	///   arrow indicators are drawn.
+	/// </para>
+	/// </remarks>
+	public class ScrollBarView : View {
+		bool vertical;
+		int size, position;
+		bool showScrollIndicator;
+		bool keepContentAlwaysInViewport = true;
+		bool autoHideScrollBars = true;
+		bool hosted;
+		ScrollBarView otherScrollBarView;
+		View contentBottomRightCorner;
+
+		bool showBothScrollIndicator => OtherScrollBarView != null && OtherScrollBarView.showScrollIndicator && showScrollIndicator;
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Absolute"/> layout.
+		/// </summary>
+		/// <param name="rect">Frame for the scrollbar.</param>
+		public ScrollBarView (Rect rect) : this (rect, 0, 0, false) { }
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Absolute"/> layout.
+		/// </summary>
+		/// <param name="rect">Frame for the scrollbar.</param>
+		/// <param name="size">The size that this scrollbar represents. Sets the <see cref="Size"/> property.</param>
+		/// <param name="position">The position within this scrollbar. Sets the <see cref="Position"/> property.</param>
+		/// <param name="isVertical">If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal. Sets the <see cref="IsVertical"/> property.</param>
+		public ScrollBarView (Rect rect, int size, int position, bool isVertical) : base (rect)
+		{
+			Init (size, position, isVertical);
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Computed"/> layout.
+		/// </summary>
+		public ScrollBarView () : this (0, 0, false) { }
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Computed"/> layout.
+		/// </summary>
+		/// <param name="size">The size that this scrollbar represents.</param>
+		/// <param name="position">The position within this scrollbar.</param>
+		/// <param name="isVertical">If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal.</param>
+		public ScrollBarView (int size, int position, bool isVertical) : base ()
+		{
+			Init (size, position, isVertical);
+		}
+
+		/// <summary>
+		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Computed"/> layout.
+		/// </summary>
+		/// <param name="host">The view that will host this scrollbar.</param>
+		/// <param name="isVertical">If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal.</param>
+		/// <param name="showBothScrollIndicator">If set to <c>true (default)</c> will have the other scrollbar, otherwise will have only one.</param>
+		public ScrollBarView (View host, bool isVertical, bool showBothScrollIndicator = true) : this (0, 0, isVertical)
+		{
+			if (host == null) {
+				throw new ArgumentNullException ("The host parameter can't be null.");
+			} else if (host.SuperView == null) {
+				throw new ArgumentNullException ("The host SuperView parameter can't be null.");
+			}
+			hosted = true;
+			ColorScheme = host.ColorScheme;
+			X = isVertical ? Pos.Right (host) - 1 : Pos.Left (host);
+			Y = isVertical ? Pos.Top (host) : Pos.Bottom (host) - 1;
+			Host = host;
+			Host.SuperView.Add (this);
+			AutoHideScrollBars = true;
+			if (showBothScrollIndicator) {
+				OtherScrollBarView = new ScrollBarView (0, 0, !isVertical) {
+					ColorScheme = host.ColorScheme,
+					Host = host,
+					OtherScrollBarView = this,
+				};
+				OtherScrollBarView.hosted = true;
+				OtherScrollBarView.X = OtherScrollBarView.IsVertical ? Pos.Right (host) - 1 : Pos.Left (host);
+				OtherScrollBarView.Y = OtherScrollBarView.IsVertical ? Pos.Top (host) : Pos.Bottom (host) - 1;
+				OtherScrollBarView.Host.SuperView.Add (OtherScrollBarView);
+				OtherScrollBarView.showScrollIndicator = true;
+			}
+			ShowScrollIndicator = true;
+			contentBottomRightCorner = new View (" ");
+			Host.SuperView.Add (contentBottomRightCorner);
+			contentBottomRightCorner.X = Pos.Right (host) - 1;
+			contentBottomRightCorner.Y = Pos.Bottom (host) - 1;
+			contentBottomRightCorner.Width = 1;
+			contentBottomRightCorner.Height = 1;
+			contentBottomRightCorner.MouseClick += ContentBottomRightCorner_MouseClick;
+		}
+
+		void ContentBottomRightCorner_MouseClick (MouseEventArgs me)
+		{
+			if (me.MouseEvent.Flags == MouseFlags.WheeledDown || me.MouseEvent.Flags == MouseFlags.WheeledUp
+				|| me.MouseEvent.Flags == MouseFlags.WheeledRight || me.MouseEvent.Flags == MouseFlags.WheeledLeft) {
+				me.Handled = true;
+				MouseEvent (me.MouseEvent);
+			} else if (me.MouseEvent.Flags == MouseFlags.Button1Clicked) {
+				me.Handled = true;
+				Host.SetFocus ();
+			}
+		}
+
+		void Init (int size, int position, bool isVertical)
+		{
+			vertical = isVertical;
+			this.position = position;
+			this.size = size;
+			WantContinuousButtonPressed = true;
+		}
+
+		/// <summary>
+		/// If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal.
+		/// </summary>
+		public bool IsVertical {
+			get => vertical;
+			set {
+				vertical = value;
+				SetNeedsDisplay ();
+			}
+		}
+
+		/// <summary>
+		/// The size of content the scrollbar represents.
+		/// </summary>
+		/// <value>The size.</value>
+		/// <remarks>The <see cref="Size"/> is typically the size of the virtual content. E.g. when a Scrollbar is
+		/// part of a <see cref="View"/> the Size is set to the appropriate dimension of <see cref="Host"/>.</remarks>
+		public int Size {
+			get => size;
+			set {
+				if (hosted || (otherScrollBarView != null && otherScrollBarView.hosted)) {
+					size = value + 1;
+				} else {
+					size = value;
+				}
+				SetNeedsDisplay ();
+			}
+		}
+
+		/// <summary>
+		/// This event is raised when the position on the scrollbar has changed.
+		/// </summary>
+		public event Action ChangedPosition;
+
+		/// <summary>
+		/// The position, relative to <see cref="Size"/>, to set the scrollbar at.
+		/// </summary>
+		/// <value>The position.</value>
+		public int Position {
+			get => position;
+			set {
+				if (position != value) {
+					if (CanScroll (value - position, out int max, vertical)) {
+						if (max == value - position) {
+							position = value;
+						} else {
+							position = Math.Max (position + max, 0);
+						}
+					} else if (max < 0) {
+						position = Math.Max (position + max, 0);
+					}
+					var s = GetBarsize (vertical);
+					if (position + s == size && (hosted || (otherScrollBarView != null && otherScrollBarView.hosted))) {
+						position++;
+					}
+					OnChangedPosition ();
+					SetNeedsDisplay ();
+				}
+			}
+		}
+
+		/// <summary>
+		/// Get or sets the view that host this <see cref="View"/>
+		/// </summary>
+		public View Host { get; internal set; }
+
+		/// <summary>
+		/// Represent a vertical or horizontal ScrollBarView other than this.
+		/// </summary>
+		public ScrollBarView OtherScrollBarView {
+			get => otherScrollBarView;
+			set {
+				if (value != null && (value.IsVertical && vertical || !value.IsVertical && !vertical)) {
+					throw new ArgumentException ($"There is already a {(vertical ? "vertical" : "horizontal")} ScrollBarView.");
+				}
+				otherScrollBarView = value;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets the visibility for the vertical or horizontal scroll indicator.
+		/// </summary>
+		/// <value><c>true</c> if show vertical or horizontal scroll indicator; otherwise, <c>false</c>.</value>
+		public bool ShowScrollIndicator {
+			get => showScrollIndicator;
+			set {
+				if (value == showScrollIndicator) {
+					return;
+				}
+
+				showScrollIndicator = value;
+				SetNeedsLayout ();
+				if (value) {
+					Visible = true;
+				} else {
+					Visible = false;
+					Position = 0;
+				}
+				SetWidthHeight ();
+			}
+		}
+
+		/// <summary>
+		/// Get or sets if the view-port is kept always visible in the area of this <see cref="ScrollBarView"/>
+		/// </summary>
+		public bool KeepContentAlwaysInViewport {
+			get { return keepContentAlwaysInViewport; }
+			set {
+				if (keepContentAlwaysInViewport != value) {
+					keepContentAlwaysInViewport = value;
+					int pos = 0;
+					if (value && !vertical && position + Host.Bounds.Width > size) {
+						pos = size - Host.Bounds.Width + (showBothScrollIndicator ? 1 : 0);
+					}
+					if (value && vertical && position + Host.Bounds.Height > size) {
+						pos = size - Host.Bounds.Height + (showBothScrollIndicator ? 1 : 0);
+					}
+					if (pos != 0) {
+						Position = pos;
+					}
+					if (OtherScrollBarView != null && OtherScrollBarView.keepContentAlwaysInViewport != value) {
+						OtherScrollBarView.KeepContentAlwaysInViewport = value;
+					}
+					if (pos == 0) {
+						Refresh ();
+					}
+				}
+			}
+		}
+
+		/// <summary>
+		/// If true the vertical/horizontal scroll bars won't be showed if it's not needed.
+		/// </summary>
+		public bool AutoHideScrollBars {
+			get => autoHideScrollBars;
+			set {
+				if (autoHideScrollBars != value) {
+					autoHideScrollBars = value;
+					SetNeedsDisplay ();
+				}
+			}
+		}
+
+		/// <summary>
+		/// Virtual method to invoke the <see cref="ChangedPosition"/> action event.
+		/// </summary>
+		public virtual void OnChangedPosition ()
+		{
+			ChangedPosition?.Invoke ();
+		}
+
+		/// <summary>
+		/// Only used for a hosted view that will update and redraw the scrollbars.
+		/// </summary>
+		public virtual void Refresh ()
+		{
+			ShowHideScrollBars ();
+		}
+
+		void ShowHideScrollBars ()
+		{
+			if (!hosted || (hosted && !autoHideScrollBars)) {
+				return;
+			}
+
+			var pending = CheckBothScrollBars (this);
+			CheckBothScrollBars (otherScrollBarView, pending);
+
+			SetWidthHeight ();
+			SetRelativeLayout (Bounds);
+			OtherScrollBarView.SetRelativeLayout (OtherScrollBarView.Bounds);
+
+			if (showBothScrollIndicator) {
+				if (contentBottomRightCorner != null) {
+					contentBottomRightCorner.Visible = true;
+				}
+			} else if (!showScrollIndicator) {
+				if (contentBottomRightCorner != null) {
+					contentBottomRightCorner.Visible = false;
+				}
+				if (Application.mouseGrabView != null && Application.mouseGrabView == this) {
+					Application.UngrabMouse ();
+				}
+			}
+			if (showScrollIndicator) {
+				Redraw (Bounds);
+			}
+			if (otherScrollBarView.showScrollIndicator) {
+				otherScrollBarView.Redraw (otherScrollBarView.Bounds);
+			}
+		}
+
+		bool CheckBothScrollBars (ScrollBarView scrollBarView, bool pending = false)
+		{
+			int barsize = scrollBarView.vertical ? scrollBarView.Bounds.Height : scrollBarView.Bounds.Width;
+
+			if (barsize == 0 || barsize > scrollBarView.size) {
+				if (scrollBarView.showScrollIndicator) {
+					scrollBarView.ShowScrollIndicator = false;
+				}
+			} else if (barsize > 0 && barsize == scrollBarView.size && scrollBarView.OtherScrollBarView != null && pending) {
+				if (scrollBarView.showScrollIndicator) {
+					scrollBarView.ShowScrollIndicator = false;
+				}
+				if (scrollBarView.OtherScrollBarView != null && scrollBarView.showBothScrollIndicator) {
+					scrollBarView.OtherScrollBarView.ShowScrollIndicator = false;
+				}
+			} else if (barsize > 0 && barsize == size && scrollBarView.OtherScrollBarView != null && !pending) {
+				pending = true;
+			} else {
+				if (scrollBarView.OtherScrollBarView != null && pending) {
+					if (!scrollBarView.showBothScrollIndicator) {
+						scrollBarView.OtherScrollBarView.ShowScrollIndicator = true;
+					}
+				}
+				if (!scrollBarView.showScrollIndicator) {
+					scrollBarView.ShowScrollIndicator = true;
+				}
+			}
+
+			return pending;
+		}
+
+		void SetWidthHeight ()
+		{
+			if (showBothScrollIndicator) {
+				Width = vertical ? 1 : Dim.Width (Host) - 1;
+				Height = vertical ? Dim.Height (Host) - 1 : 1;
+
+				otherScrollBarView.Width = otherScrollBarView.vertical ? 1 : Dim.Width (Host) - 1;
+				otherScrollBarView.Height = otherScrollBarView.vertical ? Dim.Height (Host) - 1 : 1;
+			} else if (showScrollIndicator) {
+				Width = vertical ? 1 : Dim.Width (Host) - 0;
+				Height = vertical ? Dim.Height (Host) - 0 : 1;
+			} else if (otherScrollBarView != null && otherScrollBarView.showScrollIndicator) {
+				otherScrollBarView.Width = otherScrollBarView.vertical ? 1 : Dim.Width (Host) - 0;
+				otherScrollBarView.Height = otherScrollBarView.vertical ? Dim.Height (Host) - 0 : 1;
+			}
+		}
+
+		int posTopTee;
+		int posLeftTee;
+		int posBottomTee;
+		int posRightTee;
+
+		///<inheritdoc/>
+		public override void Redraw (Rect region)
+		{
+			if (ColorScheme == null || Size == 0) {
+				return;
+			}
+
+			Driver.SetAttribute (ColorScheme.Normal);
+
+			if ((vertical && Bounds.Height == 0) || (!vertical && Bounds.Width == 0)) {
+				return;
+			}
+
+			if (vertical) {
+				if (region.Right < Bounds.Width - 1) {
+					return;
+				}
+
+				var col = Bounds.Width - 1;
+				var bh = Bounds.Height;
+				Rune special;
+
+				if (bh < 4) {
+					var by1 = position * bh / Size;
+					var by2 = (position + bh) * bh / Size;
+
+					Move (col, 0);
+					if (Bounds.Height == 1) {
+						Driver.AddRune (Driver.Diamond);
+					} else {
+						Driver.AddRune (Driver.UpArrow);
+					}
+					if (Bounds.Height == 3) {
+						Move (col, 1);
+						Driver.AddRune (Driver.Diamond);
+					}
+					if (Bounds.Height > 1) {
+						Move (col, Bounds.Height - 1);
+						Driver.AddRune (Driver.DownArrow);
+					}
+				} else {
+					bh -= 2;
+					var by1 = KeepContentAlwaysInViewport ? position * bh / Size : position * bh / (Size + bh);
+					var by2 = KeepContentAlwaysInViewport ? Math.Min (((position + bh) * bh / Size) + 1, bh - 1) : (position + bh) * bh / (Size + bh);
+					if (KeepContentAlwaysInViewport && by1 == by2) {
+						by1 = Math.Max (by1 - 1, 0);
+					}
+
+					Move (col, 0);
+					Driver.AddRune (Driver.UpArrow);
+					Move (col, Bounds.Height - 1);
+					Driver.AddRune (Driver.DownArrow);
+
+					bool hasTopTee = false;
+					bool hasDiamond = false;
+					bool hasBottomTee = false;
+					for (int y = 0; y < bh; y++) {
+						Move (col, y + 1);
+						if ((y < by1 || y > by2) && ((position > 0 && !hasTopTee) || (hasTopTee && hasBottomTee))) {
+							special = Driver.Stipple;
+						} else {
+							if (y != by2 && y > 1 && by2 - by1 == 0 && by1 < bh - 1 && hasTopTee && !hasDiamond) {
+								hasDiamond = true;
+								special = Driver.Diamond;
+							} else {
+								if (y == by1 && !hasTopTee) {
+									hasTopTee = true;
+									posTopTee = y;
+									special = Driver.TopTee;
+								} else if ((position == 0 && y == bh - 1 || y >= by2 || by2 == 0) && !hasBottomTee) {
+									hasBottomTee = true;
+									posBottomTee = y;
+									special = Driver.BottomTee;
+								} else {
+									special = Driver.VLine;
+								}
+							}
+						}
+						Driver.AddRune (special);
+					}
+					if (!hasTopTee) {
+						Move (col, Bounds.Height - 2);
+						Driver.AddRune (Driver.TopTee);
+					}
+				}
+			} else {
+				if (region.Bottom < Bounds.Height - 1) {
+					return;
+				}
+
+				var row = Bounds.Height - 1;
+				var bw = Bounds.Width;
+				Rune special;
+
+				if (bw < 4) {
+					var bx1 = position * bw / Size;
+					var bx2 = (position + bw) * bw / Size;
+
+					Move (0, row);
+					Driver.AddRune (Driver.LeftArrow);
+					Driver.AddRune (Driver.RightArrow);
+				} else {
+					bw -= 2;
+					var bx1 = KeepContentAlwaysInViewport ? position * bw / Size : position * bw / (Size + bw);
+					var bx2 = KeepContentAlwaysInViewport ? Math.Min (((position + bw) * bw / Size) + 1, bw - 1) : (position + bw) * bw / (Size + bw);
+					if (KeepContentAlwaysInViewport && bx1 == bx2) {
+						bx1 = Math.Max (bx1 - 1, 0);
+					}
+
+					Move (0, row);
+					Driver.AddRune (Driver.LeftArrow);
+
+					bool hasLeftTee = false;
+					bool hasDiamond = false;
+					bool hasRightTee = false;
+					for (int x = 0; x < bw; x++) {
+						if ((x < bx1 || x >= bx2 + 1) && ((position > 0 && !hasLeftTee) || (hasLeftTee && hasRightTee))) {
+							special = Driver.Stipple;
+						} else {
+							if (x != bx2 && x > 1 && bx2 - bx1 == 0 && bx1 < bw - 1 && hasLeftTee && !hasDiamond) {
+								hasDiamond = true;
+								special = Driver.Diamond;
+							} else {
+								if (x == bx1 && !hasLeftTee) {
+									hasLeftTee = true;
+									posLeftTee = x;
+									special = Driver.LeftTee;
+								} else if ((position == 0 && x == bw - 1 || x >= bx2 || bx2 == 0) && !hasRightTee) {
+									hasRightTee = true;
+									posRightTee = x;
+									special = Driver.RightTee;
+								} else {
+									special = Driver.HLine;
+								}
+							}
+						}
+						Driver.AddRune (special);
+					}
+					if (!hasLeftTee) {
+						Move (Bounds.Width - 2, row);
+						Driver.AddRune (Driver.LeftTee);
+					}
+
+					Driver.AddRune (Driver.RightArrow);
+				}
+			}
+
+			if (contentBottomRightCorner != null && hosted && showBothScrollIndicator) {
+				contentBottomRightCorner.Redraw (contentBottomRightCorner.Bounds);
+			}
+		}
+
+		int lastLocation = -1;
+		int posBarOffset;
+
+		///<inheritdoc/>
+		public override bool MouseEvent (MouseEvent me)
+		{
+			if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1DoubleClicked &&
+				!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) &&
+				me.Flags != MouseFlags.Button1Released && me.Flags != MouseFlags.WheeledDown &&
+				me.Flags != MouseFlags.WheeledUp && me.Flags != MouseFlags.WheeledRight &&
+				me.Flags != MouseFlags.WheeledLeft && me.Flags != MouseFlags.Button1TripleClicked) {
+				return false;
+			}
+
+			if (Host != null && !Host.HasFocus) {
+				Host.SetFocus ();
+			}
+
+			int location = vertical ? me.Y : me.X;
+			int barsize = vertical ? Bounds.Height : Bounds.Width;
+			int posTopLeftTee = vertical ? posTopTee + 1 : posLeftTee + 1;
+			int posBottomRightTee = vertical ? posBottomTee + 1 : posRightTee + 1;
+			barsize -= 2;
+			var pos = Position;
+
+			if (me.Flags != MouseFlags.Button1Released
+				&& (Application.mouseGrabView == null || Application.mouseGrabView != this)) {
+				Application.GrabMouse (this);
+			} else if (me.Flags == MouseFlags.Button1Released && Application.mouseGrabView != null && Application.mouseGrabView == this) {
+				lastLocation = -1;
+				Application.UngrabMouse ();
+				return true;
+			}
+			if (showScrollIndicator && (me.Flags == MouseFlags.WheeledDown || me.Flags == MouseFlags.WheeledUp ||
+				me.Flags == MouseFlags.WheeledRight || me.Flags == MouseFlags.WheeledLeft)) {
+				return Host.MouseEvent (me);
+			}
+
+			if (location == 0) {
+				if (pos > 0) {
+					Position = pos - 1;
+				}
+			} else if (location == barsize + 1) {
+				if (CanScroll (1, out _, vertical)) {
+					Position = pos + 1;
+				}
+			} else if (location > 0 && location < barsize + 1) {
+				//var b1 = pos * (Size > 0 ? barsize / Size : 0);
+				//var b2 = Size > 0
+				//	? (KeepContentAlwaysInViewport ? Math.Min (((pos + barsize) * barsize / Size) + 1, barsize - 1) : (pos + barsize) * barsize / Size)
+				//	: 0;
+				//if (KeepContentAlwaysInViewport && b1 == b2) {
+				//	b1 = Math.Max (b1 - 1, 0);
+				//}
+
+				if (lastLocation > -1 || (location >= posTopLeftTee && location <= posBottomRightTee
+				&& me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) {
+					if (lastLocation == -1) {
+						lastLocation = location;
+						posBarOffset = keepContentAlwaysInViewport ? Math.Max (location - posTopLeftTee, 1) : 0;
+						return true;
+					}
+
+					if (location > lastLocation) {
+						if (location - posBarOffset < barsize) {
+							var np = ((location - posBarOffset) * Size / barsize) + (Size / barsize);
+							if (CanScroll (np - pos, out int nv, vertical)) {
+								Position = pos + nv;
+							}
+						} else if (CanScroll (Size - pos, out int nv, vertical)) {
+							Position = Math.Min (pos + nv, Size);
+						}
+					} else if (location < lastLocation) {
+						if (location - posBarOffset > 0) {
+							var np = ((location - posBarOffset) * Size / barsize) - (Size / barsize);
+							if (CanScroll (np - pos, out int nv, vertical)) {
+								Position = pos + nv;
+							}
+						} else {
+							Position = 0;
+						}
+					} else if (location - posBarOffset >= barsize && posBottomRightTee - posTopLeftTee >= 3 && CanScroll (Size - pos, out int nv, vertical)) {
+						Position = Math.Min (pos + nv, Size);
+					} else if (location - posBarOffset >= barsize - 1 && posBottomRightTee - posTopLeftTee <= 3 && CanScroll (Size - pos, out nv, vertical)) {
+						Position = Math.Min (pos + nv, Size);
+					} else if (location - posBarOffset <= 0 && posBottomRightTee - posTopLeftTee <= 3) {
+						Position = 0;
+					}
+				} else if (location > posBottomRightTee) {
+					if (CanScroll (barsize, out int nv, vertical)) {
+						Position = pos + nv;
+					}
+				} else if (location < posTopLeftTee) {
+					if (CanScroll (-barsize, out int nv, vertical)) {
+						Position = pos + nv;
+					}
+				} else if (location == 1 && posTopLeftTee <= 3) {
+					Position = 0;
+				} else if (location == barsize) {
+					if (CanScroll (Size - pos, out int nv, vertical)) {
+						Position = Math.Min (pos + nv, Size);
+					}
+				}
+			}
+
+			return true;
+		}
+
+		internal bool CanScroll (int n, out int max, bool isVertical = false)
+		{
+			if (Host == null) {
+				max = 0;
+				return false;
+			}
+			int s = GetBarsize (isVertical);
+			var newSize = Math.Max (Math.Min (size - s, position + n), 0);
+			max = size > s + newSize ? (newSize == 0 ? -position : n) : size - (s + position) - 1;
+			if (size >= s + newSize && max != 0) {
+				return true;
+			}
+			return false;
+		}
+
+		int GetBarsize (bool isVertical)
+		{
+			if (Host == null) {
+				return 0;
+			}
+			return isVertical ?
+				(KeepContentAlwaysInViewport ? Host.Bounds.Height + (showBothScrollIndicator ? -2 : -1) : 0) :
+				(KeepContentAlwaysInViewport ? Host.Bounds.Width + (showBothScrollIndicator ? -2 : -1) : 0);
+		}
+	}
+}

+ 35 - 366
Terminal.Gui/Views/ScrollView.cs

@@ -1,5 +1,5 @@
 //
-// ScrollView.cs: ScrollView and ScrollBarView views.
+// ScrollView.cs: ScrollView view.
 //
 // Authors:
 //   Miguel de Icaza ([email protected])
@@ -15,345 +15,6 @@ using System;
 using System.Reflection;
 
 namespace Terminal.Gui {
-	/// <summary>
-	/// ScrollBarViews are views that display a 1-character scrollbar, either horizontal or vertical
-	/// </summary>
-	/// <remarks>
-	/// <para>
-	///   The scrollbar is drawn to be a representation of the Size, assuming that the 
-	///   scroll position is set at Position.
-	/// </para>
-	/// <para>
-	///   If the region to display the scrollbar is larger than three characters, 
-	///   arrow indicators are drawn.
-	/// </para>
-	/// </remarks>
-	public class ScrollBarView : View {
-		bool vertical = false;
-		int size = 0, position = 0;
-
-		/// <summary>
-		/// If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal.
-		/// </summary>
-		public bool IsVertical {
-			get => vertical;
-			set {
-				vertical = value;
-				SetNeedsDisplay ();
-			}
-		}
-
-		/// <summary>
-		/// The size of content the scrollbar represents. 
-		/// </summary>
-		/// <value>The size.</value>
-		/// <remarks>The <see cref="Size"/> is typically the size of the virtual content. E.g. when a Scrollbar is
-		/// part of a <see cref="ScrollView"/> the Size is set to the appropriate dimension of <see cref="ScrollView.ContentSize"/>.</remarks>
-		public int Size {
-			get => size;
-			set {
-				size = value;
-				SetNeedsDisplay ();
-			}
-		}
-
-		/// <summary>
-		/// This event is raised when the position on the scrollbar has changed.
-		/// </summary>
-		public event Action ChangedPosition;
-
-		/// <summary>
-		/// The position, relative to <see cref="Size"/>, to set the scrollbar at.
-		/// </summary>
-		/// <value>The position.</value>
-		public int Position {
-			get => position;
-			set {
-				position = value;
-				SetNeedsDisplay ();
-			}
-		}
-
-		/// <summary>
-		/// Get or sets the view that host this <see cref="ScrollView"/>
-		/// </summary>
-		public ScrollView Host { get; internal set; }
-
-		void SetPosition (int newPos)
-		{
-			Position = newPos;
-			ChangedPosition?.Invoke ();
-		}
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Absolute"/> layout.
-		/// </summary>
-		/// <param name="rect">Frame for the scrollbar.</param>
-		public ScrollBarView (Rect rect) : this (rect, 0, 0, false) { }
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Absolute"/> layout.
-		/// </summary>
-		/// <param name="rect">Frame for the scrollbar.</param>
-		/// <param name="size">The size that this scrollbar represents. Sets the <see cref="Size"/> property.</param>
-		/// <param name="position">The position within this scrollbar. Sets the <see cref="Position"/> property.</param>
-		/// <param name="isVertical">If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal. Sets the <see cref="IsVertical"/> property.</param>
-		public ScrollBarView (Rect rect, int size, int position, bool isVertical) : base (rect)
-		{
-			Init (size, position, isVertical);
-		}
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Computed"/> layout.
-		/// </summary>
-		public ScrollBarView () : this (0, 0, false) { }
-
-		/// <summary>
-		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class using <see cref="LayoutStyle.Computed"/> layout.
-		/// </summary>
-		/// <param name="size">The size that this scrollbar represents.</param>
-		/// <param name="position">The position within this scrollbar.</param>
-		/// <param name="isVertical">If set to <c>true</c> this is a vertical scrollbar, otherwise, the scrollbar is horizontal.</param>
-		public ScrollBarView (int size, int position, bool isVertical) : base ()
-		{
-			Init (size, position, isVertical);
-		}
-
-		void Init (int size, int position, bool isVertical)
-		{
-			vertical = isVertical;
-			this.position = position;
-			this.size = size;
-			WantContinuousButtonPressed = true;
-		}
-
-		int posTopTee;
-		int posLeftTee;
-		int posBottomTee;
-		int posRightTee;
-
-		///<inheritdoc/>
-		public override void Redraw (Rect region)
-		{
-			if (ColorScheme == null || Size == 0)
-				return;
-
-			Driver.SetAttribute (ColorScheme.Normal);
-
-			if (Bounds.Height == 0) {
-				return;
-			}
-
-			if (vertical) {
-				if (region.Right < Bounds.Width - 1)
-					return;
-
-				var col = Bounds.Width - 1;
-				var bh = Bounds.Height;
-				Rune special;
-
-				if (bh < 4) {
-					var by1 = position * bh / Size;
-					var by2 = (position + bh) * bh / Size;
-
-					Move (col, 0);
-					if (Bounds.Height == 1) {
-						Driver.AddRune (Driver.Diamond);
-					} else {
-						Driver.AddRune (Driver.UpArrow);
-					}
-					if (Bounds.Height == 3) {
-						Move (col, 1);
-						Driver.AddRune (Driver.Diamond);
-					}
-					if (Bounds.Height > 1) {
-						Move (col, Bounds.Height - 1);
-						Driver.AddRune (Driver.DownArrow);
-					}
-				} else {
-					bh -= 2;
-					var by1 = position * bh / Size;
-					var by2 = Host.KeepContentAlwaysInViewport ? Math.Min (((position + bh) * bh / Size) + 1, bh - 1) : (position + bh) * bh / Size;
-					if (Host.KeepContentAlwaysInViewport && by1 == by2) {
-						by1 = Math.Max (by1 - 1, 0);
-					}
-
-					Move (col, 0);
-					Driver.AddRune (Driver.UpArrow);
-					Move (col, Bounds.Height - 1);
-					Driver.AddRune (Driver.DownArrow);
-
-					bool hasTopTee = false;
-					bool hasDiamond = false;
-					bool hasBottomTee = false;
-					for (int y = 0; y < bh; y++) {
-						Move (col, y + 1);
-						if ((y < by1 || y > by2) && ((position > 0 && !hasTopTee) || (hasTopTee && hasBottomTee))) {
-							special = Driver.Stipple;
-						} else {
-							if (y != by2 && y > 1 && by2 - by1 == 0 && by1 < bh - 1 && hasTopTee && !hasDiamond) {
-								hasDiamond = true;
-								special = Driver.Diamond;
-							} else {
-								if (y == by1 && !hasTopTee) {
-									hasTopTee = true;
-									posTopTee = y;
-									special = Driver.TopTee;
-								} else if ((position == 0 && y == bh - 1 || y >= by2 || by2 == 0) && !hasBottomTee) {
-									hasBottomTee = true;
-									posBottomTee = y;
-									special = Driver.BottomTee;
-								} else {
-									special = Driver.VLine;
-								}
-							}
-						}
-						Driver.AddRune (special);
-					}
-					if (!hasTopTee) {
-						Move (col, Bounds.Height - 2);
-						Driver.AddRune (Driver.TopTee);
-					}
-				}
-			} else {
-				if (region.Bottom < Bounds.Height - 1)
-					return;
-
-				var row = Bounds.Height - 1;
-				var bw = Bounds.Width;
-				Rune special;
-
-				if (bw < 4) {
-					var bx1 = position * bw / Size;
-					var bx2 = (position + bw) * bw / Size;
-
-					Move (0, row);
-					Driver.AddRune (Driver.LeftArrow);
-					Driver.AddRune (Driver.RightArrow);
-				} else {
-					bw -= 2;
-					var bx1 = position * bw / Size;
-					var bx2 = Host.KeepContentAlwaysInViewport ? Math.Min (((position + bw) * bw / Size) + 1, bw - 1) : (position + bw) * bw / Size;
-					if (Host.KeepContentAlwaysInViewport && bx1 == bx2) {
-						bx1 = Math.Max (bx1 - 1, 0);
-					}
-
-					Move (0, row);
-					Driver.AddRune (Driver.LeftArrow);
-
-					bool hasLeftTee = false;
-					bool hasDiamond = false;
-					bool hasRightTee = false;
-					for (int x = 0; x < bw; x++) {
-						if ((x < bx1 || x >= bx2 + 1) && ((position > 0 && !hasLeftTee) || (hasLeftTee && hasRightTee))) {
-							special = Driver.Stipple;
-						} else {
-							if (x != bx2 && x > 1 && bx2 - bx1 == 0 && bx1 < bw - 1 && hasLeftTee && !hasDiamond) {
-								hasDiamond = true;
-								special = Driver.Diamond;
-							} else {
-								if (x == bx1 && !hasLeftTee) {
-									hasLeftTee = true;
-									posLeftTee = x;
-									special = Driver.LeftTee;
-								} else if ((position == 0 && x == bw - 1 || x >= bx2 || bx2 == 0) && !hasRightTee) {
-									hasRightTee = true;
-									posRightTee = x;
-									special = Driver.RightTee;
-								} else {
-									special = Driver.HLine;
-								}
-							}
-						}
-						Driver.AddRune (special);
-					}
-					if (!hasLeftTee) {
-						Move (Bounds.Width -2, row);
-						Driver.AddRune (Driver.LeftTee);
-					}
-
-					Driver.AddRune (Driver.RightArrow);
-				}
-			}
-		}
-
-		int lastLocation = -1;
-
-		///<inheritdoc/>
-		public override bool MouseEvent (MouseEvent me)
-		{
-			if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked &&
-				!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
-				return false;
-			}
-
-			if (!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
-				lastLocation = -1;
-			}
-
-			int location = vertical ? me.Y : me.X;
-			int barsize = vertical ? Bounds.Height : Bounds.Width;
-			int posTopLeftTee = vertical ? posTopTee : posLeftTee;
-			int posBottomRightTee = vertical ? posBottomTee : posRightTee;
-
-			barsize -= 2;
-			var pos = Position;
-			if (location == 0) {
-				if (pos > 0) {
-					SetPosition (pos - 1);
-				}
-			} else if (location == barsize + 1) {
-				if (Host.CanScroll (1, out _, vertical)) {
-					SetPosition (pos + 1);
-				}
-			} else if (location > 0 && location < barsize + 1) {
-				var b1 = pos * barsize / Size;
-				var b2 = Host.KeepContentAlwaysInViewport ? Math.Min (((pos + barsize) * barsize / Size) + 1, barsize - 1) : (pos + barsize) * barsize / Size;
-				if (Host.KeepContentAlwaysInViewport && b1 == b2) {
-					b1 = Math.Max (b1 - 1, 0);
-				}
-
-				if (location > b1 && location <= b2 + 1) {
-					if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1Clicked) {
-						if (location == 1) {
-							SetPosition (0);
-						} else if (location == barsize) {
-							Host.CanScroll (Size - pos, out int nv, vertical);
-							if (nv > 0) {
-								SetPosition (Math.Min (pos + nv, Size));
-							}
-						}
-					} else if (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) {
-						var mb = (b2 - b1) / 2;
-						var ml = mb + b1 + (mb == 0 ? 1 : 0);
-						if ((location >= b1 && location <= ml) || (location < lastLocation && lastLocation > -1)) {
-							lastLocation = location;
-							var np = b1 * Size / barsize;
-							SetPosition (np);
-						} else if (location > lastLocation) {
-							var np = location * Size / barsize;
-							Host.CanScroll (np - pos, out int nv, vertical);
-							if (nv > 0) {
-								SetPosition (pos + nv);
-							}
-						}
-					}
-				} else {
-					if (location >= b2 + 1 && location > posTopLeftTee && location > b1 && location > posBottomRightTee && posBottomRightTee > 0) {
-						Host.CanScroll (location, out int nv, vertical);
-						if (nv > 0) {
-							SetPosition (Math.Min (pos + nv, Size));
-						}
-					} else if (location <= b1) {
-						SetPosition (Math.Max (pos - barsize - location, 0));
-					}
-				}
-			}
-
-			return true;
-		}
-	}
-
 	/// <summary>
 	/// Scrollviews are views that present a window into a virtual space where subviews are added.  Similar to the iOS UIScrollView.
 	/// </summary>
@@ -417,6 +78,8 @@ namespace Terminal.Gui {
 
 			MouseEnter += View_MouseEnter;
 			MouseLeave += View_MouseLeave;
+			contentView.MouseEnter += View_MouseEnter;
+			contentView.MouseLeave += View_MouseLeave;
 		}
 
 		Size contentSize;
@@ -454,11 +117,20 @@ namespace Terminal.Gui {
 				return contentOffset;
 			}
 			set {
-				contentOffset = new Point (-Math.Abs (value.X), -Math.Abs (value.Y));
-				contentView.Frame = new Rect (contentOffset, contentSize);
-				vertical.Position = Math.Max (0, -contentOffset.Y);
-				horizontal.Position = Math.Max (0, -contentOffset.X);
-				SetNeedsDisplay ();
+				var co = new Point (-Math.Abs (value.X), -Math.Abs (value.Y));
+				if (contentOffset != co) {
+					contentOffset = co;
+					contentView.Frame = new Rect (contentOffset, contentSize);
+					var p = Math.Max (0, -contentOffset.Y);
+					if (vertical.Position != p) {
+						vertical.Position = Math.Max (0, -contentOffset.Y);
+					}
+					p = Math.Max (0, -contentOffset.X);
+					if (horizontal.Position != p) {
+						horizontal.Position = Math.Max (0, -contentOffset.X);
+					}
+					SetNeedsDisplay ();
+				}
 			}
 		}
 
@@ -483,6 +155,8 @@ namespace Terminal.Gui {
 			set {
 				if (keepContentAlwaysInViewport != value) {
 					keepContentAlwaysInViewport = value;
+					vertical.OtherScrollBarView.KeepContentAlwaysInViewport = value;
+					horizontal.OtherScrollBarView.KeepContentAlwaysInViewport = value;
 					Point p = default;
 					if (value && -contentOffset.X + Bounds.Width > contentSize.Width) {
 						p = new Point (contentSize.Width - Bounds.Width + (showVerticalScrollIndicator ? 1 : 0), -contentOffset.Y);
@@ -517,7 +191,9 @@ namespace Terminal.Gui {
 
 		void View_MouseLeave (MouseEventArgs e)
 		{
-			Application.UngrabMouse ();
+			if (Application.mouseGrabView != null && Application.mouseGrabView != vertical && Application.mouseGrabView != horizontal) {
+				Application.UngrabMouse ();
+			}
 		}
 
 		void View_MouseEnter (MouseEventArgs e)
@@ -540,17 +216,21 @@ namespace Terminal.Gui {
 		public bool ShowHorizontalScrollIndicator {
 			get => showHorizontalScrollIndicator;
 			set {
-				if (value == showHorizontalScrollIndicator)
+				if (value == showHorizontalScrollIndicator) {
 					return;
+				}
 
 				showHorizontalScrollIndicator = value;
 				SetNeedsLayout ();
 				if (value) {
 					base.Add (horizontal);
+					horizontal.OtherScrollBarView = vertical;
+					horizontal.OtherScrollBarView.ShowScrollIndicator = value;
 					horizontal.MouseEnter += View_MouseEnter;
 					horizontal.MouseLeave += View_MouseLeave;
 				} else {
-					Remove (horizontal);
+					base.Remove (horizontal);
+					horizontal.OtherScrollBarView = null;
 					horizontal.MouseEnter -= View_MouseEnter;
 					horizontal.MouseLeave -= View_MouseLeave;
 				}
@@ -575,17 +255,21 @@ namespace Terminal.Gui {
 		public bool ShowVerticalScrollIndicator {
 			get => showVerticalScrollIndicator;
 			set {
-				if (value == showVerticalScrollIndicator)
+				if (value == showVerticalScrollIndicator) {
 					return;
+				}
 
 				showVerticalScrollIndicator = value;
 				SetNeedsLayout ();
 				if (value) {
 					base.Add (vertical);
+					vertical.OtherScrollBarView = horizontal;
+					vertical.OtherScrollBarView.ShowScrollIndicator = value;
 					vertical.MouseEnter += View_MouseEnter;
 					vertical.MouseLeave += View_MouseLeave;
 				} else {
 					Remove (vertical);
+					vertical.OtherScrollBarView = null;
 					vertical.MouseEnter -= View_MouseEnter;
 					vertical.MouseLeave -= View_MouseLeave;
 				}
@@ -739,7 +423,7 @@ namespace Terminal.Gui {
 		/// <param name="lines">Number of lines to scroll.</param>
 		public bool ScrollDown (int lines)
 		{
-			if (CanScroll (lines, out _, true)) {
+			if (vertical.CanScroll (lines, out _, true)) {
 				ContentOffset = new Point (contentOffset.X, contentOffset.Y - lines);
 				return true;
 			}
@@ -753,28 +437,13 @@ namespace Terminal.Gui {
 		/// <param name="cols">Number of columns to scroll by.</param>
 		public bool ScrollRight (int cols)
 		{
-			if (CanScroll (cols, out _)) {
+			if (horizontal.CanScroll (cols, out _)) {
 				ContentOffset = new Point (contentOffset.X - cols, contentOffset.Y);
 				return true;
 			}
 			return false;
 		}
 
-		internal bool CanScroll (int n, out int max, bool isVertical = false)
-		{
-			var size = isVertical ?
-				(KeepContentAlwaysInViewport ? Bounds.Height + (showHorizontalScrollIndicator ? -2 : -1) : 0) :
-				(KeepContentAlwaysInViewport ? Bounds.Width + (showVerticalScrollIndicator ? -2 : -1) : 0);
-			var cSize = isVertical ? -contentSize.Height : -contentSize.Width;
-			var cOffSet = isVertical ? contentOffset.Y : contentOffset.X;
-			var newSize = Math.Max (cSize, cOffSet - n);
-			max = cSize < newSize - size ? n : -cSize + (cOffSet - size) - 1;
-			if (cSize < newSize - size) {
-				return true;
-			}
-			return false;
-		}
-
 		///<inheritdoc/>
 		public override bool ProcessKey (KeyEvent kb)
 		{

+ 25 - 3
Terminal.Gui/Views/TextView.cs

@@ -460,6 +460,26 @@ namespace Terminal.Gui {
 			}
 		}
 
+		/// <summary>
+		/// Gets or sets the top row.
+		/// </summary>
+		public int TopRow { get => topRow; set => topRow = Math.Max (Math.Min (value, Lines - 1), 0); }
+
+		/// <summary>
+		/// Gets or sets the left column.
+		/// </summary>
+		public int LeftColumn { get => leftColumn; set => leftColumn = Math.Max (Math.Min (value, Maxlength - 1), 0); }
+
+		/// <summary>
+		/// Gets the maximum visible length line.
+		/// </summary>
+		public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height);
+
+		/// <summary>
+		/// Gets the  number of lines.
+		/// </summary>
+		public int Lines => model.Count;
+
 		/// <summary>
 		/// Loads the contents of the file into the  <see cref="TextView"/>.
 		/// </summary>
@@ -875,10 +895,10 @@ namespace Terminal.Gui {
 				idx = 0;
 			}
 			if (isRow) {
-				topRow = idx > model.Count - 1 ? model.Count - 1 : idx;
+				topRow = Math.Max (idx > model.Count - 1 ? model.Count - 1 : idx, 0);
 			} else {
 				var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height);
-				leftColumn = idx > maxlength - 1 ? maxlength - 1 : idx;
+				leftColumn = Math.Max (idx > maxlength - 1 ? maxlength - 1 : idx, 0);
 			}
 			SetNeedsDisplay ();
 		}
@@ -1374,7 +1394,7 @@ namespace Terminal.Gui {
 
 			if (ev.Flags == MouseFlags.Button1Clicked) {
 				if (model.Count > 0) {
-					var maxCursorPositionableLine = (model.Count - 1) - topRow;
+					var maxCursorPositionableLine = Math.Max ((model.Count - 1) - topRow, 0);
 					if (ev.Y > maxCursorPositionableLine) {
 						currentRow = maxCursorPositionableLine;
 					} else {
@@ -1389,6 +1409,8 @@ namespace Terminal.Gui {
 					}
 				}
 				PositionCursor ();
+				lastWasKill = false;
+				columnTrack = currentColumn;
 			} else if (ev.Flags == MouseFlags.WheeledDown) {
 				lastWasKill = false;
 				columnTrack = currentColumn;

+ 43 - 0
UICatalog/Scenarios/Editor.cs

@@ -13,6 +13,7 @@ namespace UICatalog {
 		private string _fileName = "demo.txt";
 		private TextView _textView;
 		private bool _saved = true;
+		private ScrollBarView _scrollBar;
 
 		public override void Init (Toplevel top, ColorScheme colorScheme)
 		{
@@ -35,6 +36,7 @@ namespace UICatalog {
 					new MenuItem ("C_ut", "", () => Cut()),
 					new MenuItem ("_Paste", "", () => Paste())
 				}),
+				new MenuBarItem ("_ScrollBarView", CreateKeepChecked ())
 			});
 			Top.Add (menu);
 
@@ -67,6 +69,33 @@ namespace UICatalog {
 			LoadFile ();
 
 			Win.Add (_textView);
+
+			_scrollBar = new ScrollBarView (_textView, true);
+
+			_scrollBar.ChangedPosition += () => {
+				_textView.TopRow = _scrollBar.Position;
+				if (_textView.TopRow != _scrollBar.Position) {
+					_scrollBar.Position = _textView.TopRow;
+				}
+				_textView.SetNeedsDisplay ();
+			};
+
+			_scrollBar.OtherScrollBarView.ChangedPosition += () => {
+				_textView.LeftColumn = _scrollBar.OtherScrollBarView.Position;
+				if (_textView.LeftColumn != _scrollBar.OtherScrollBarView.Position) {
+					_scrollBar.OtherScrollBarView.Position = _textView.LeftColumn;
+				}
+				_textView.SetNeedsDisplay ();
+			};
+
+			_textView.DrawContent += (e) => {
+				_scrollBar.Size = _textView.Lines - 1;
+				_scrollBar.Position = _textView.TopRow;
+				_scrollBar.OtherScrollBarView.Size = _textView.Maxlength;
+				_scrollBar.OtherScrollBarView.Position = _textView.LeftColumn;
+				_scrollBar.LayoutSubviews ();
+				_scrollBar.Refresh ();
+			};
 		}
 
 		public override void Setup ()
@@ -145,11 +174,25 @@ namespace UICatalog {
 			sb.Append ("Hello world.\n");
 			sb.Append ("This is a test of the Emergency Broadcast System.\n");
 
+			for (int i = 0; i < 30; i++) {
+				sb.Append ($"{i} - This is a test with a very long line and many lines to test the ScrollViewBar against the TextView. - {i}\n");
+			}
 			var sw = System.IO.File.CreateText (fileName);
 			sw.Write (sb.ToString ());
 			sw.Close ();
 		}
 
+		private MenuItem [] CreateKeepChecked ()
+		{
+			var item = new MenuItem ();
+			item.Title = "Keep Content Always In Viewport";
+			item.CheckType |= MenuItemCheckStyle.Checked;
+			item.Checked = true;
+			item.Action += () => _scrollBar.KeepContentAlwaysInViewport = item.Checked = !item.Checked;
+
+			return new MenuItem [] { item };
+		}
+
 		public override void Run ()
 		{
 			base.Run ();

+ 64 - 10
UICatalog/Scenarios/ListViewWithSelection.cs

@@ -6,7 +6,7 @@ using System.Linq;
 using Terminal.Gui;
 
 namespace UICatalog {
-	[ScenarioMetadata (Name: "List View With Selection", Description: "ListView with colunns and selection")]
+	[ScenarioMetadata (Name: "List View With Selection", Description: "ListView with columns and selection")]
 	[ScenarioCategory ("Controls")]
 	class ListViewWithSelection : Scenario {
 
@@ -25,7 +25,7 @@ namespace UICatalog {
 				Height = 1,
 			};
 			Win.Add (_customRenderCB);
-			_customRenderCB.Toggled += _customRenderCB_Toggled; ;
+			_customRenderCB.Toggled += _customRenderCB_Toggled;
 
 			_allowMarkingCB = new CheckBox ("Allow Marking") {
 				X = Pos.Right (_customRenderCB) + 1,
@@ -55,9 +55,41 @@ namespace UICatalog {
 			};
 			Win.Add (_listView);
 
-			
+			var _scrollBar = new ScrollBarView (_listView, true);
+
+			_scrollBar.ChangedPosition += () => {
+				_listView.TopItem = _scrollBar.Position;
+				if (_listView.TopItem != _scrollBar.Position) {
+					_scrollBar.Position = _listView.TopItem;
+				}
+				_listView.SetNeedsDisplay ();
+			};
+
+			_scrollBar.OtherScrollBarView.ChangedPosition += () => {
+				_listView.LeftItem = _scrollBar.OtherScrollBarView.Position;
+				if (_listView.LeftItem != _scrollBar.OtherScrollBarView.Position) {
+					_scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
+				}
+				_listView.SetNeedsDisplay ();
+			};
+
+			_listView.DrawContent += (e) => {
+				_scrollBar.Size = _listView.Source.Count - 1;
+				_scrollBar.Position = _listView.TopItem;
+				_scrollBar.OtherScrollBarView.Size = _listView.Maxlength - 1;
+				_scrollBar.OtherScrollBarView.Position = _listView.LeftItem;
+				_scrollBar.Refresh ();
+			};
+
 			_listView.SetSource (_scenarios);
 
+			var k = "Keep Content Always In Viewport";
+			var keepCheckBox = new CheckBox (k, _scrollBar.AutoHideScrollBars) {
+				X = Pos.AnchorEnd (k.Length + 3),
+				Y = 0,
+			};
+			keepCheckBox.Toggled += (_) => _scrollBar.KeepContentAlwaysInViewport = keepCheckBox.Checked;
+			Win.Add (keepCheckBox);
 		}
 
 		private void _customRenderCB_Toggled (bool prev)
@@ -84,20 +116,21 @@ namespace UICatalog {
 			Win.SetNeedsDisplay ();
 		}
 
-		// This is basicaly the same implementation used by the UICatalog main window
+		// This is basically the same implementation used by the UICatalog main window
 		internal class ScenarioListDataSource : IListDataSource {
 			int _nameColumnWidth = 30;
 			private List<Type> scenarios;
 			BitArray marks;
-			int count;
+			int count, len;
 
 			public List<Type> Scenarios {
-				get => scenarios; 
+				get => scenarios;
 				set {
 					if (value != null) {
 						count = value.Count;
 						marks = new BitArray (count);
 						scenarios = value;
+						len = GetMaxLengthItem ();
 					}
 				}
 			}
@@ -110,14 +143,16 @@ namespace UICatalog {
 
 			public int Count => Scenarios != null ? Scenarios.Count : 0;
 
+			public int Length => len;
+
 			public ScenarioListDataSource (List<Type> itemList) => Scenarios = itemList;
 
-			public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width)
+			public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0)
 			{
 				container.Move (col, line);
 				// Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible
 				var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item]));
-				RenderUstr (driver, $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width);
+				RenderUstr (driver, $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width, start);
 			}
 
 			public void SetMark (int item, bool value)
@@ -126,11 +161,30 @@ namespace UICatalog {
 					marks [item] = value;
 			}
 
+			int GetMaxLengthItem ()
+			{
+				if (scenarios?.Count == 0) {
+					return 0;
+				}
+
+				int maxLength = 0;
+				for (int i = 0; i < scenarios.Count; i++) {
+					var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [i]));
+					var sc = $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [i])}";
+					var l = sc.Length;
+					if (l > maxLength) {
+						maxLength = l;
+					}
+				}
+
+				return maxLength;
+			}
+
 			// A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
-			private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width)
+			private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
 			{
 				int used = 0;
-				int index = 0;
+				int index = start;
 				while (index < ustr.Length) {
 					(var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length);
 					var count = Rune.ColumnWidth (rune);

+ 53 - 0
UICatalog/Scenarios/ListsAndCombos.cs

@@ -39,6 +39,32 @@ namespace UICatalog.Scenarios {
 			listview.SelectedItemChanged += (ListViewItemEventArgs e) => lbListView.Text = items [listview.SelectedItem];
 			Win.Add (lbListView, listview);
 
+			var _scrollBar = new ScrollBarView (listview, true);
+
+			_scrollBar.ChangedPosition += () => {
+				listview.TopItem = _scrollBar.Position;
+				if (listview.TopItem != _scrollBar.Position) {
+					_scrollBar.Position = listview.TopItem;
+				}
+				listview.SetNeedsDisplay ();
+			};
+
+			_scrollBar.OtherScrollBarView.ChangedPosition += () => {
+				listview.LeftItem = _scrollBar.OtherScrollBarView.Position;
+				if (listview.LeftItem != _scrollBar.OtherScrollBarView.Position) {
+					_scrollBar.OtherScrollBarView.Position = listview.LeftItem;
+				}
+				listview.SetNeedsDisplay ();
+			};
+
+			listview.DrawContent += (e) => {
+				_scrollBar.Size = listview.Source.Count - 1;
+				_scrollBar.Position = listview.TopItem;
+				_scrollBar.OtherScrollBarView.Size = listview.Maxlength - 1;
+				_scrollBar.OtherScrollBarView.Position = listview.LeftItem;
+				_scrollBar.Refresh ();
+			};
+
 			// ComboBox
 			var lbComboBox = new Label ("ComboBox") {
 				ColorScheme = Colors.TopLevel,
@@ -57,6 +83,33 @@ namespace UICatalog.Scenarios {
 			comboBox.SelectedItemChanged += (ListViewItemEventArgs text) => lbComboBox.Text = items[comboBox.SelectedItem];
 			Win.Add (lbComboBox, comboBox);
 
+			var scrollBarCbx = new ScrollBarView (comboBox.Subviews [1], true);
+
+			scrollBarCbx.ChangedPosition += () => {
+				((ListView)comboBox.Subviews [1]).TopItem = scrollBarCbx.Position;
+				if (((ListView)comboBox.Subviews [1]).TopItem != scrollBarCbx.Position) {
+					scrollBarCbx.Position = ((ListView)comboBox.Subviews [1]).TopItem;
+				}
+				comboBox.SetNeedsDisplay ();
+			};
+
+			scrollBarCbx.OtherScrollBarView.ChangedPosition += () => {
+				((ListView)comboBox.Subviews [1]).LeftItem = scrollBarCbx.OtherScrollBarView.Position;
+				if (((ListView)comboBox.Subviews [1]).LeftItem != scrollBarCbx.OtherScrollBarView.Position) {
+					scrollBarCbx.OtherScrollBarView.Position = ((ListView)comboBox.Subviews [1]).LeftItem;
+				}
+				comboBox.SetNeedsDisplay ();
+			};
+
+			comboBox.DrawContent += (e) => {
+				scrollBarCbx.Size = comboBox.Source.Count;
+				scrollBarCbx.Position = ((ListView)comboBox.Subviews [1]).TopItem;
+				scrollBarCbx.OtherScrollBarView.Size = ((ListView)comboBox.Subviews [1]).Maxlength - 1;
+				scrollBarCbx.OtherScrollBarView.Position = ((ListView)comboBox.Subviews [1]).LeftItem;
+				scrollBarCbx.Refresh ();
+			};
+
+
 			var btnMoveUp = new Button ("Move _Up") {
 				X = 1,
 				Y = Pos.Bottom(lbListView),

+ 32 - 5
UICatalog/UICatalog.cs

@@ -498,31 +498,58 @@ namespace UICatalog {
 		}
 
 		internal class ScenarioListDataSource : IListDataSource {
+			private readonly int len;
+
 			public List<Type> Scenarios { get; set; }
 
 			public bool IsMarked (int item) => false;
 
 			public int Count => Scenarios.Count;
 
-			public ScenarioListDataSource (List<Type> itemList) => Scenarios = itemList;
+			public int Length => len;
+
+			public ScenarioListDataSource (List<Type> itemList)
+			{
+				Scenarios = itemList;
+				len = GetMaxLengthItem ();
+			}
 
-			public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width)
+			public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0)
 			{
 				container.Move (col, line);
 				// Equivalent to an interpolated string like $"{Scenarios[item].Name, -widtestname}"; if such a thing were possible
 				var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [item]));
-				RenderUstr (driver, $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width);
+				RenderUstr (driver, $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [item])}", col, line, width, start);
 			}
 
 			public void SetMark (int item, bool value)
 			{
 			}
 
+			int GetMaxLengthItem ()
+			{
+				if (Scenarios?.Count == 0) {
+					return 0;
+				}
+
+				int maxLength = 0;
+				for (int i = 0; i < Scenarios.Count; i++) {
+					var s = String.Format (String.Format ("{{0,{0}}}", -_nameColumnWidth), Scenario.ScenarioMetadata.GetName (Scenarios [i]));
+					var sc = $"{s}  {Scenario.ScenarioMetadata.GetDescription (Scenarios [i])}";
+					var l = sc.Length;
+					if (l > maxLength) {
+						maxLength = l;
+					}
+				}
+
+				return maxLength;
+			}
+
 			// A slightly adapted method from: https://github.com/migueldeicaza/gui.cs/blob/fc1faba7452ccbdf49028ac49f0c9f0f42bbae91/Terminal.Gui/Views/ListView.cs#L433-L461
-			private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width)
+			private void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
 			{
 				int used = 0;
-				int index = 0;
+				int index = start;
 				while (index < ustr.Length) {
 					(var rune, var size) = Utf8.DecodeRune (ustr, index, index - ustr.Length);
 					var count = Rune.ColumnWidth (rune);

+ 408 - 0
UnitTests/ScrollBarViewTests.cs

@@ -0,0 +1,408 @@
+using System;
+using Xunit;
+
+namespace Terminal.Gui {
+	public class ScrollBarViewTests {
+		public class HostView : View {
+			public int Top { get; set; }
+			public int Lines { get; set; }
+			public int Left { get; set; }
+			public int Cols { get; set; }
+		}
+
+		private HostView _hostView;
+		private ScrollBarView _scrollBar;
+		private bool _added;
+
+		public ScrollBarViewTests ()
+		{
+			Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true)));
+
+			var top = Application.Top;
+
+			_hostView = new HostView () {
+				Width = Dim.Fill (),
+				Height = Dim.Fill (),
+				Top = 0,
+				Lines = 30,
+				Left = 0,
+				Cols = 100
+			};
+
+			top.Add (_hostView);
+		}
+
+		private void AddHandlers ()
+		{
+			if (!_added) {
+				_hostView.DrawContent += _hostView_DrawContent;
+				_scrollBar.ChangedPosition += _scrollBar_ChangedPosition;
+				_scrollBar.OtherScrollBarView.ChangedPosition += _scrollBar_OtherScrollBarView_ChangedPosition;
+			}
+			_added = true;
+		}
+
+		private void RemoveHandlers ()
+		{
+			if (_added) {
+				_hostView.DrawContent -= _hostView_DrawContent;
+				_scrollBar.ChangedPosition -= _scrollBar_ChangedPosition;
+				_scrollBar.OtherScrollBarView.ChangedPosition -= _scrollBar_OtherScrollBarView_ChangedPosition;
+			}
+			_added = false;
+		}
+
+		private void _hostView_DrawContent (Rect obj)
+		{
+			_scrollBar.Size = _hostView.Lines;
+			_scrollBar.Position = _hostView.Top;
+			_scrollBar.OtherScrollBarView.Size = _hostView.Cols;
+			_scrollBar.OtherScrollBarView.Position = _hostView.Left;
+			_scrollBar.Refresh ();
+		}
+
+		private void _scrollBar_ChangedPosition ()
+		{
+			_hostView.Top = _scrollBar.Position;
+			if (_hostView.Top != _scrollBar.Position) {
+				_scrollBar.Position = _hostView.Top;
+			}
+			_hostView.SetNeedsDisplay ();
+		}
+
+		private void _scrollBar_OtherScrollBarView_ChangedPosition ()
+		{
+			_hostView.Left = _scrollBar.OtherScrollBarView.Position;
+			if (_hostView.Left != _scrollBar.OtherScrollBarView.Position) {
+				_scrollBar.OtherScrollBarView.Position = _hostView.Left;
+			}
+			_hostView.SetNeedsDisplay ();
+		}
+
+		[Fact]
+		public void Hosting_A_Null_View_To_A_ScrollBarView_Throws_ArgumentNullException ()
+		{
+			Assert.Throws<ArgumentNullException> ("The host parameter can't be null.",
+				() => new ScrollBarView (null, true));
+			Assert.Throws<ArgumentNullException> ("The host parameter can't be null.",
+				() => new ScrollBarView (null, false));
+		}
+
+		[Fact]
+		public void Hosting_A_Null_SuperView_View_To_A_ScrollBarView_Throws_ArgumentNullException ()
+		{
+			Assert.Throws<ArgumentNullException> ("The host SuperView parameter can't be null.",
+				() => new ScrollBarView (new View (), true));
+			Assert.Throws<ArgumentNullException> ("The host SuperView parameter can't be null.",
+				() => new ScrollBarView (new View (), false));
+		}
+
+		[Fact]
+		public void Hosting_Two_Vertical_ScrollBarView_Throws_ArgumentException ()
+		{
+			var top = new Toplevel ();
+			var host = new View ();
+			top.Add (host);
+			var v = new ScrollBarView (host, true);
+			var h = new ScrollBarView (host, true);
+
+			Assert.Throws<ArgumentException> (null, () => v.OtherScrollBarView = h);
+			Assert.Throws<ArgumentException> (null, () => h.OtherScrollBarView = v);
+		}
+
+		[Fact]
+		public void Hosting_Two_Horizontal_ScrollBarView_Throws_ArgumentException ()
+		{
+			var top = new Toplevel ();
+			var host = new View ();
+			top.Add (host);
+			var v = new ScrollBarView (host, false);
+			var h = new ScrollBarView (host, false);
+
+			Assert.Throws<ArgumentException> (null, () => v.OtherScrollBarView = h);
+			Assert.Throws<ArgumentException> (null, () => h.OtherScrollBarView = v);
+		}
+
+		[Fact]
+		public void Scrolling_With_Default_Constructor_Do_Not_Scroll ()
+		{
+			var sbv = new ScrollBarView {
+				Position = 1
+			};
+			Assert.NotEqual (1, sbv.Position);
+			Assert.Equal (0, sbv.Position);
+		}
+
+		[Fact]
+		public void Hosting_A_View_To_A_ScrollBarView ()
+		{
+			RemoveHandlers ();
+
+			_scrollBar = new ScrollBarView (_hostView, true);
+
+			Assert.True (_scrollBar.IsVertical);
+			Assert.False (_scrollBar.OtherScrollBarView.IsVertical);
+
+			Assert.Equal (_scrollBar.Position, _hostView.Top);
+			Assert.NotEqual (_scrollBar.Size, _hostView.Lines);
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+			Assert.NotEqual (_scrollBar.OtherScrollBarView.Size, _hostView.Cols);
+
+			AddHandlers ();
+			_hostView.SuperView.LayoutSubviews ();
+			_hostView.Redraw (_hostView.Bounds);
+
+			Assert.Equal (_scrollBar.Position, _hostView.Top);
+			Assert.Equal (_scrollBar.Size, _hostView.Lines + 1);
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+			Assert.Equal (_scrollBar.OtherScrollBarView.Size, _hostView.Cols + 1);
+		}
+
+		[Fact]
+		public void ChangedPosition_Update_The_Hosted_View ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			_scrollBar.Position = 2;
+			Assert.Equal (_scrollBar.Position, _hostView.Top);
+
+			_scrollBar.OtherScrollBarView.Position = 5;
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+		}
+
+		[Fact]
+		public void ChangedPosition_Scrolling ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			for (int i = 0; i < _scrollBar.Size; i++) {
+				_scrollBar.Position += 1;
+				Assert.Equal (_scrollBar.Position, _hostView.Top);
+			}
+			for (int i = _scrollBar.Size - 1; i >= 0; i--) {
+				_scrollBar.Position -= 1;
+				Assert.Equal (_scrollBar.Position, _hostView.Top);
+			}
+
+			for (int i = 0; i < _scrollBar.OtherScrollBarView.Size; i++) {
+				_scrollBar.OtherScrollBarView.Position += i;
+				Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+			}
+			for (int i = _scrollBar.OtherScrollBarView.Size - 1; i >= 0; i--) {
+				_scrollBar.OtherScrollBarView.Position -= 1;
+				Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+			}
+		}
+
+		[Fact]
+		public void ChangedPosition_Negative_Value ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			_scrollBar.Position = -20;
+			Assert.Equal (0, _scrollBar.Position);
+			Assert.Equal (_scrollBar.Position, _hostView.Top);
+
+			_scrollBar.OtherScrollBarView.Position = -50;
+			Assert.Equal (0, _scrollBar.OtherScrollBarView.Position);
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+		}
+
+		[Fact]
+		public void DrawContent_Update_The_ScrollBarView_Position ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			_hostView.Top = 3;
+			_hostView.Redraw (_hostView.Bounds);
+			Assert.Equal (_scrollBar.Position, _hostView.Top);
+
+			_hostView.Left = 6;
+			_hostView.Redraw (_hostView.Bounds);
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+		}
+
+		[Fact]
+		public void OtherScrollBarView_Not_Null ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			Assert.NotNull (_scrollBar.OtherScrollBarView);
+			Assert.NotEqual (_scrollBar, _scrollBar.OtherScrollBarView);
+			Assert.Equal (_scrollBar.OtherScrollBarView.OtherScrollBarView, _scrollBar);
+		}
+
+		[Fact]
+		public void ShowScrollIndicator_Check ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			Assert.True (_scrollBar.ShowScrollIndicator);
+			Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+		}
+
+		[Fact]
+		public void KeepContentAlwaysInViewport_True ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			Assert.Equal (80, _hostView.Bounds.Width);
+			Assert.Equal (25, _hostView.Bounds.Height);
+			Assert.Equal (79, _scrollBar.OtherScrollBarView.Bounds.Width);
+			Assert.Equal (24, _scrollBar.Bounds.Height);
+			Assert.Equal (31, _scrollBar.Size);
+			Assert.Equal (101, _scrollBar.OtherScrollBarView.Size);
+			Assert.True (_scrollBar.ShowScrollIndicator);
+			Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.True (_scrollBar.Visible);
+			Assert.True (_scrollBar.OtherScrollBarView.Visible);
+
+			_scrollBar.Position = 50;
+			Assert.Equal (_scrollBar.Position, _scrollBar.Size - _scrollBar.Bounds.Height);
+			Assert.Equal (_scrollBar.Position, _hostView.Top);
+			Assert.Equal (7, _scrollBar.Position);
+			Assert.Equal (7, _hostView.Top);
+			Assert.True (_scrollBar.ShowScrollIndicator);
+			Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.True (_scrollBar.Visible);
+			Assert.True (_scrollBar.OtherScrollBarView.Visible);
+
+			_scrollBar.OtherScrollBarView.Position = 150;
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _scrollBar.OtherScrollBarView.Size - _scrollBar.OtherScrollBarView.Bounds.Width);
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+			Assert.Equal (22, _scrollBar.OtherScrollBarView.Position);
+			Assert.Equal (22, _hostView.Left);
+			Assert.True (_scrollBar.ShowScrollIndicator);
+			Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.True (_scrollBar.Visible);
+			Assert.True (_scrollBar.OtherScrollBarView.Visible);
+		}
+
+		[Fact]
+		public void KeepContentAlwaysInViewport_False ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			_scrollBar.KeepContentAlwaysInViewport = false;
+			_scrollBar.Position = 50;
+			Assert.Equal (_scrollBar.Position, _scrollBar.Size - 1);
+			Assert.Equal (_scrollBar.Position, _hostView.Top);
+			Assert.Equal (30, _scrollBar.Position);
+			Assert.Equal (30, _hostView.Top);
+
+			_scrollBar.OtherScrollBarView.Position = 150;
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _scrollBar.OtherScrollBarView.Size - 1);
+			Assert.Equal (_scrollBar.OtherScrollBarView.Position, _hostView.Left);
+			Assert.Equal (100, _scrollBar.OtherScrollBarView.Position);
+			Assert.Equal (100, _hostView.Left);
+		}
+
+		[Fact]
+		public void AutoHideScrollBars_Check ()
+		{
+			Hosting_A_View_To_A_ScrollBarView ();
+
+			AddHandlers ();
+
+			_hostView.Redraw (_hostView.Bounds);
+			Assert.True (_scrollBar.ShowScrollIndicator);
+			Assert.True (_scrollBar.Visible);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.Width.ToString ());
+			Assert.Equal (1, _scrollBar.Bounds.Width);
+			Assert.Equal ("Dim.Combine(DimView(side=Height, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(1))",
+				_scrollBar.Height.ToString ());
+			Assert.Equal (24, _scrollBar.Bounds.Height);
+			Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.True (_scrollBar.OtherScrollBarView.Visible);
+			Assert.Equal ("Dim.Combine(DimView(side=Width, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(1))",
+				_scrollBar.OtherScrollBarView.Width.ToString ());
+			Assert.Equal (79, _scrollBar.OtherScrollBarView.Bounds.Width);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
+			Assert.Equal (1, _scrollBar.OtherScrollBarView.Bounds.Height);
+
+			_hostView.Lines = 10;
+			_hostView.Redraw (_hostView.Bounds);
+			Assert.False (_scrollBar.ShowScrollIndicator);
+			Assert.False (_scrollBar.Visible);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.Width.ToString ());
+			Assert.Equal (1, _scrollBar.Bounds.Width);
+			Assert.Equal ("Dim.Combine(DimView(side=Height, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(1))",
+				_scrollBar.Height.ToString ());
+			Assert.Equal (24, _scrollBar.Bounds.Height);
+			Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.True (_scrollBar.OtherScrollBarView.Visible);
+			Assert.Equal ("Dim.Combine(DimView(side=Width, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(0))",
+				_scrollBar.OtherScrollBarView.Width.ToString ());
+			Assert.Equal (80, _scrollBar.OtherScrollBarView.Bounds.Width);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
+			Assert.Equal (1, _scrollBar.OtherScrollBarView.Bounds.Height);
+
+			_hostView.Cols = 60;
+			_hostView.Redraw (_hostView.Bounds);
+			Assert.False (_scrollBar.ShowScrollIndicator);
+			Assert.False (_scrollBar.Visible);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.Width.ToString ());
+			Assert.Equal (1, _scrollBar.Bounds.Width);
+			Assert.Equal ("Dim.Combine(DimView(side=Height, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(1))",
+				_scrollBar.Height.ToString ());
+			Assert.Equal (24, _scrollBar.Bounds.Height);
+			Assert.False (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.False (_scrollBar.OtherScrollBarView.Visible);
+			Assert.Equal ("Dim.Combine(DimView(side=Width, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(0))",
+				_scrollBar.OtherScrollBarView.Width.ToString ());
+			Assert.Equal (80, _scrollBar.OtherScrollBarView.Bounds.Width);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
+			Assert.Equal (1, _scrollBar.OtherScrollBarView.Bounds.Height);
+
+			_hostView.Lines = 40;
+			_hostView.Redraw (_hostView.Bounds);
+			Assert.True (_scrollBar.ShowScrollIndicator);
+			Assert.True (_scrollBar.Visible);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.Width.ToString ());
+			Assert.Equal (1, _scrollBar.Bounds.Width);
+			Assert.Equal ("Dim.Combine(DimView(side=Height, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(0))",
+				_scrollBar.Height.ToString ());
+			Assert.Equal (25, _scrollBar.Bounds.Height);
+			Assert.False (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.False (_scrollBar.OtherScrollBarView.Visible);
+			Assert.Equal ("Dim.Combine(DimView(side=Width, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(0))",
+				_scrollBar.OtherScrollBarView.Width.ToString ());
+			Assert.Equal (80, _scrollBar.OtherScrollBarView.Bounds.Width);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
+			Assert.Equal (1, _scrollBar.OtherScrollBarView.Bounds.Height);
+
+			_hostView.Cols = 120;
+			_hostView.Redraw (_hostView.Bounds);
+			Assert.True (_scrollBar.ShowScrollIndicator);
+			Assert.True (_scrollBar.Visible);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.Width.ToString ());
+			Assert.Equal (1, _scrollBar.Bounds.Width);
+			Assert.Equal ("Dim.Combine(DimView(side=Height, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(1))",
+				_scrollBar.Height.ToString ());
+			Assert.Equal (24, _scrollBar.Bounds.Height);
+			Assert.True (_scrollBar.OtherScrollBarView.ShowScrollIndicator);
+			Assert.True (_scrollBar.OtherScrollBarView.Visible);
+			Assert.Equal ("Dim.Combine(DimView(side=Width, target=HostView()({X=0,Y=0,Width=80,Height=25}))-Dim.Absolute(1))",
+				_scrollBar.OtherScrollBarView.Width.ToString ());
+			Assert.Equal (79, _scrollBar.OtherScrollBarView.Bounds.Width);
+			Assert.Equal ("Dim.Absolute(1)", _scrollBar.OtherScrollBarView.Height.ToString ());
+			Assert.Equal (1, _scrollBar.OtherScrollBarView.Bounds.Height);
+		}
+	}
+}