2
0
Эх сурвалжийг харах

Upgraded ScrollView + Charmap (#601)

Note this PR should not be merged until after #600 is in. 

I went on a rampage tonight. It all started with wanting to use more/better characters for frame and other UI elements like the round corners:

![image](https://user-images.githubusercontent.com/585482/83601742-659ba800-a52e-11ea-9ee9-c888a7db5444.png)

I decided I needed a character map app that would let me test which fonts had which Unicode sets in them.

As a result we have this PR

- Fixes `ScrollView` in several key ways:
   - It now supports Computed layout and has constructors that don't require parameters.
   - `ScrollBarViews` are now positioned using Computed layout versus error prone absoulte
   - `ScrollBarViews` now correctly position themselves when one, either, or both are on/off.
   - `IsVertical` is now a public property that does the expected thing when changed
   - Mouse handling is better; there's still a bug where the mouse doesn't get grabbed by the `ScrollView` initially but I think this is a broader problem. I need @BDisp's help on this.

- The `Scrolling` Scenario was enhanced to demo dynamically adding/removing horizontal/vertical scrollbars (and to prove it was working right).

- I Enabled easy "infinite scroll capability" - CharMap literally lets you scroll over `int.MaxValue / 16` rows of data. Filling a `ContentView` with all of this and panning it around won't work. So I needed a way of having `Redraw` give me virtual coordinates. I did this by defining `OnDrawContent(Rect viewport)` and it's associated `event`:

```csharp
/// <summary>
/// Event invoked when the content area of the View is to be drawn.
/// </summary>
/// <remarks>
/// <para>
/// Will be invoked before any subviews added with <see cref="Add(View)"/> have been drawn.
/// </para>
/// <para>
/// Rect provides the view-relative rectangle describing the currently visible viewport into the <see cref="View"/>.
/// </para>
/// </remarks>
public event EventHandler<Rect> DrawContent;

/// <summary>
/// Enables overrides to draw infinitely scrolled content and/or a background behind added controls. 
/// </summary>
/// <param name="viewport">The view-relative rectangle describing the currently visible viewport into the <see cref="View"/></param>
/// <remarks>
/// This method will be called before any subviews added with <see cref="Add(View)"/> have been drawn. 
/// </remarks>
public virtual void OnDrawContent (Rect viewport)
{
	DrawContent?.Invoke (this, viewport);
}

```

I originally just implemented this pattern in `ScrollView`. Then I realized I wanted the same thing out of ALL `Views`. Namely: the ability to do drawing on an event, particularly to be able to paint something in the background. So I added it to `View`.

Note, that these changes mean we are about 3 small steps away from moving the scollbars from `ScrollView` into ALL views. Which makes a lot of sense to me because I don't think we want to implement duplicative logic in, say `ListView` and `TextView` as well. Why not just do it once?

Along the way I fixed some other things:

- The `Checkbox.Toggled` event now passes state. 

Here's some gifs. 
![](https://i.imgur.com/o5nP5Lo.gif)

Note:

- Scrollbars appear dynamically.
- Fast scrolling of huge data (using no memory).
- Static header
- Dynamic scrollbars on/off
- Note the bottom/right corner now draw correctly in all situations
Charlie Kindel 5 жил өмнө
parent
commit
7a0c522a20

+ 2 - 2
Example/demo.cs

@@ -335,12 +335,12 @@ static class Demo {
 			$"{mi.Title.ToString ()} selected. Is from submenu: {mi.GetMenuBarItem ()}", "Ok");
 	}
 
-	static void MenuKeysStyle_Toggled (object sender, EventArgs e)
+	static void MenuKeysStyle_Toggled (object sender, bool e)
 	{
 		menu.UseKeysUpDownAsKeysLeftRight = menuKeysStyle.Checked;
 	}
 
-	static void MenuAutoMouseNav_Toggled (object sender, EventArgs e)
+	static void MenuAutoMouseNav_Toggled (object sender, bool e)
 	{
 		menu.WantMousePositionReports = menuAutoMouseNav.Checked;
 	}

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

@@ -929,6 +929,9 @@ namespace Terminal.Gui {
 		{
 			var clipRect = new Rect (Point.Empty, frame.Size);
 
+			// Invoke DrawContentEvent
+			OnDrawContent (bounds);
+
 			if (subviews != null) {
 				foreach (var view in subviews) {
 					if (view.NeedDisplay != null && (!view.NeedDisplay.IsEmpty || view.childNeedsDisplay)) {
@@ -941,7 +944,11 @@ namespace Terminal.Gui {
 
 							// Clip the sub-view
 							var savedClip = ClipToBounds ();
+
+							// Draw the subview
 							view.Redraw (view.Bounds);
+
+							// Undo the clip
 							Driver.Clip = savedClip;
 						}
 						view.NeedDisplay = Rect.Empty;
@@ -952,6 +959,31 @@ namespace Terminal.Gui {
 			ClearNeedsDisplay ();
 		}
 
+		/// <summary>
+		/// Event invoked when the content area of the View is to be drawn.
+		/// </summary>
+		/// <remarks>
+		/// <para>
+		/// Will be invoked before any subviews added with <see cref="Add(View)"/> have been drawn.
+		/// </para>
+		/// <para>
+		/// Rect provides the view-relative rectangle describing the currently visible viewport into the <see cref="View"/>.
+		/// </para>
+		/// </remarks>
+		public event EventHandler<Rect> DrawContent;
+
+		/// <summary>
+		/// Enables overrides to draw infinitely scrolled content and/or a background behind added controls. 
+		/// </summary>
+		/// <param name="viewport">The view-relative rectangle describing the currently visible viewport into the <see cref="View"/></param>
+		/// <remarks>
+		/// This method will be called before any subviews added with <see cref="Add(View)"/> have been drawn. 
+		/// </remarks>
+		public virtual void OnDrawContent (Rect viewport)
+		{
+			DrawContent?.Invoke (this, viewport);
+		}
+
 		/// <summary>
 		/// Causes the specified subview to have focus.
 		/// </summary>

+ 9 - 0
Terminal.Gui/Terminal.Gui.csproj

@@ -78,6 +78,15 @@
       * More robust error handing in Pos/Dim. Fixes #355 stack overflow with Pos based on the size of windows at startup. Added a OnResized action to set the Pos after the terminal are resized. (Thanks @bdisp!)
       * Fixes #389 Window layouting breaks when resizing. (Thanks @bdisp!)
       * Fixes #557 MessageBox needs to take ustrings (BREAKING CHANGE). (Thanks @tig!)
+      * Fixes ScrollView in several key ways. (Thanks @tig!)
+      *   Now supports Computed layout and has constructors that don't require parameters.
+      *   ScrollBarViews are now positioned using Computed layout versus error prone absoulte
+      *   ScrollBarViews now correctly position themselves when one, either, or both are on/off.
+      *   IsVertical is now a public property that does the expected thing when changed
+      *   Mouse handling is better; there's still a bug where the mouse doesn't get grabbed by the ScrollView initially but I think this is a broader problem. I need @BDisp's help on this.
+      *   Supports "infinite scrolling" via the new OnDrawContent/DrawContent event on the View class.
+      *   The Scrolling Scenario was enhanced to demo dynamically adding/removing horizontal/vertical scrollbars (and to prove it was working right).
+      * The Checkbox.Toggled event is now an EventHandler event and passes previous state. (Thanks @tig!)
 
       0.81:
       * Fix ncurses engine for macOS/Linux, it works again

+ 13 - 8
Terminal.Gui/Views/Checkbox.cs

@@ -23,9 +23,16 @@ namespace Terminal.Gui {
 		/// <remarks>
 		///   Client code can hook up to this event, it is
 		///   raised when the <see cref="CheckBox"/> is activated either with
-		///   the mouse or the keyboard.
+		///   the mouse or the keyboard. The passed <c>bool</c> contains the previous state. 
 		/// </remarks>
-		public event EventHandler Toggled;
+		public event EventHandler<bool> Toggled;
+
+		/// <summary>
+		/// Called when the <see cref="Checked"/> property changes. Invokes the <see cref="Toggled"/> event.
+		/// </summary>
+		public virtual void OnToggled (bool previousChecked) {
+			Toggled?.Invoke (this, previousChecked);
+		}
 
 		/// <summary>
 		/// Initializes a new instance of <see cref="CheckBox"/> based on the given text, uses Computed layout and sets the height and width.
@@ -122,11 +129,9 @@ namespace Terminal.Gui {
 		public override bool ProcessKey (KeyEvent kb)
 		{
 			if (kb.KeyValue == ' ') {
+				var previousChecked = Checked;
 				Checked = !Checked;
-
-				if (Toggled != null)
-					Toggled (this, EventArgs.Empty);
-
+				OnToggled (previousChecked);
 				SetNeedsDisplay ();
 				return true;
 			}
@@ -140,11 +145,11 @@ namespace Terminal.Gui {
 				return false;
 
 			SuperView.SetFocus (this);
+			var previousChecked = Checked;
 			Checked = !Checked;
+			OnToggled (previousChecked);
 			SetNeedsDisplay ();
 
-			if (Toggled != null)
-				Toggled (this, EventArgs.Empty);
 			return true;
 		}
 	}

+ 129 - 49
Terminal.Gui/Views/ScrollView.cs

@@ -6,9 +6,7 @@
 //
 //
 // TODO:
-// - Mouse handling in scrollbarview
 // - focus in scrollview
-// - keyboard handling in scrollview to scroll
 // - focus handling in scrollview to auto scroll to focused view
 // - Raise events
 // - Perhaps allow an option to not display the scrollbar arrow indicators?
@@ -31,13 +29,26 @@ namespace Terminal.Gui {
 	/// </para>
 	/// </remarks>
 	public class ScrollBarView : View {
-		bool vertical;
-		int size, position;
+		bool vertical = false;
+		int size = 0, position = 0;
 
 		/// <summary>
-		/// The size that this scrollbar represents
+		/// 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 {
@@ -52,7 +63,7 @@ namespace Terminal.Gui {
 		public event Action ChangedPosition;
 
 		/// <summary>
-		/// The position to show the scrollbar at.
+		/// The position, relative to <see cref="Size"/>, to set the scrollbar at.
 		/// </summary>
 		/// <value>The position.</value>
 		public int Position {
@@ -70,13 +81,40 @@ namespace Terminal.Gui {
 		}
 
 		/// <summary>
-		/// Initializes a new instance of the <see cref="Gui.ScrollBarView"/> class.
+		/// 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 (Rect rect, int size, int position, bool isVertical) : base (rect)
+		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;
@@ -84,11 +122,8 @@ namespace Terminal.Gui {
 			WantContinuousButtonPressed = true;
 		}
 
-		/// <summary>
-		/// Redraw the scrollbar
-		/// </summary>
-		/// <param name="region">Region to be redrawn.</param>
-		public override void Redraw(Rect region)
+		///<inheritdoc cref="Redraw(Rect)"/>
+		public override void Redraw (Rect region)
 		{
 			if (ColorScheme == null)
 				return;
@@ -113,7 +148,7 @@ namespace Terminal.Gui {
 							special = Driver.Stipple;
 						else
 							special = Driver.Diamond;
-						Driver.AddRune(special);
+						Driver.AddRune (special);
 					}
 				} else {
 					bh -= 2;
@@ -125,7 +160,7 @@ namespace Terminal.Gui {
 					Move (col, Bounds.Height - 1);
 					Driver.AddRune ('v');
 					for (int y = 0; y < bh; y++) {
-						Move (col, y+1);
+						Move (col, y + 1);
 						if (y < by1 - 1 || y > by2)
 							special = Driver.Stipple;
 						else {
@@ -195,7 +230,7 @@ namespace Terminal.Gui {
 		}
 
 		///<inheritdoc cref="MouseEvent"/>
-		public override bool MouseEvent(MouseEvent me)
+		public override bool MouseEvent (MouseEvent me)
 		{
 			if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked &&
 				!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
@@ -239,39 +274,66 @@ namespace Terminal.Gui {
 	}
 
 	/// <summary>
-	/// Scrollviews are views that present a window into a virtual space where children views are added.  Similar to the iOS UIScrollView.
+	/// Scrollviews are views that present a window into a virtual space where subviews are added.  Similar to the iOS UIScrollView.
 	/// </summary>
 	/// <remarks>
 	/// <para>
-	///   The subviews that are added to this scrollview are offset by the
-	///   ContentOffset property.   The view itself is a window into the 
-	///   space represented by the ContentSize.
+	///   The subviews that are added to this <see cref="Gui.ScrollView"/> are offset by the
+	///   <see cref="ContentOffset"/> property.  The view itself is a window into the 
+	///   space represented by the <see cref="ContentSize"/>.
 	/// </para>
 	/// <para>
-	///   
+	///   Use the 
 	/// </para>
 	/// </remarks>
 	public class ScrollView : View {
-		View contentView;
+		View contentView = null;
 		ScrollBarView vertical, horizontal;
 
 		/// <summary>
-		/// Constructs a ScrollView
+		///  Initializes a new instance of the <see cref="Gui.ScrollView"/> class using <see cref="LayoutStyle.Absolute"/> positioning.
 		/// </summary>
 		/// <param name="frame"></param>
 		public ScrollView (Rect frame) : base (frame)
+		{
+			Init (frame);
+		}
+
+
+		/// <summary>
+		///  Initializes a new instance of the <see cref="Gui.ScrollView"/> class using <see cref="LayoutStyle.Computed"/> positioning.
+		/// </summary>
+		public ScrollView () : base ()
+		{
+			Init (new Rect (0, 0, 0, 0));
+		}
+
+		void Init (Rect frame)
 		{
 			contentView = new View (frame);
-			vertical = new ScrollBarView (new Rect (frame.Width - 1, 0, 1, frame.Height), frame.Height, 0, isVertical: true);
+			vertical = new ScrollBarView (1, 0, isVertical: true) {
+				X = Pos.AnchorEnd (1),
+				Y = 0,
+				Width = 1,
+				Height = Dim.Fill (showHorizontalScrollIndicator ? 1 : 0)
+			};
 			vertical.ChangedPosition += delegate {
 				ContentOffset = new Point (ContentOffset.X, vertical.Position);
 			};
-			horizontal = new ScrollBarView (new Rect (0, frame.Height-1, frame.Width-1, 1), frame.Width-1, 0, isVertical: false);
+			horizontal = new ScrollBarView (1, 0, isVertical: false) {
+				X = 0,
+				Y = Pos.AnchorEnd (1),
+				Width = Dim.Fill (showVerticalScrollIndicator ? 1 : 0),
+				Height = 1
+			};
 			horizontal.ChangedPosition += delegate {
 				ContentOffset = new Point (horizontal.Position, ContentOffset.Y);
 			};
 			base.Add (contentView);
 			CanFocus = true;
+
+			MouseEnter += View_MouseEnter;
+			MouseLeave += View_MouseLeave;
 		}
 
 		Size contentSize;
@@ -305,7 +367,7 @@ namespace Terminal.Gui {
 				return contentOffset;
 			}
 			set {
-				contentOffset = new Point (-Math.Abs (value.X), -Math.Abs(value.Y));
+				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);
@@ -322,12 +384,9 @@ namespace Terminal.Gui {
 			if (!IsOverridden (view)) {
 				view.MouseEnter += View_MouseEnter;
 				view.MouseLeave += View_MouseLeave;
-				vertical.MouseEnter += View_MouseEnter;
-				vertical.MouseLeave += View_MouseLeave;
-				horizontal.MouseEnter += View_MouseEnter;
-				horizontal.MouseLeave += View_MouseLeave;
 			}
 			contentView.Add (view);
+			SetNeedsLayout ();
 		}
 
 		void View_MouseLeave (object sender, MouseEventEventArgs e)
@@ -359,11 +418,17 @@ namespace Terminal.Gui {
 					return;
 
 				showHorizontalScrollIndicator = value;
-				SetNeedsDisplay ();
-				if (value)
+				SetNeedsLayout ();
+				if (value) {
 					base.Add (horizontal);
-				else
+					horizontal.MouseEnter += View_MouseEnter;
+					horizontal.MouseLeave += View_MouseLeave;
+				} else {
 					Remove (horizontal);
+					horizontal.MouseEnter -= View_MouseEnter;
+					horizontal.MouseLeave -= View_MouseLeave;
+				}
+				vertical.Height = Dim.Fill (showHorizontalScrollIndicator ? 1 : 0);
 			}
 		}
 
@@ -372,9 +437,9 @@ namespace Terminal.Gui {
 		/// </summary>
 		/// <remarks>
 		/// </remarks>
-		public override void RemoveAll()
+		public override void RemoveAll ()
 		{
-			contentView.RemoveAll();
+			contentView.RemoveAll ();
 		}
 
 		/// <summary>
@@ -388,31 +453,46 @@ namespace Terminal.Gui {
 					return;
 
 				showVerticalScrollIndicator = value;
-				SetNeedsDisplay ();
-				if (value)
+				SetNeedsLayout ();
+				if (value) {
 					base.Add (vertical);
-				else
+					vertical.MouseEnter += View_MouseEnter;
+					vertical.MouseLeave += View_MouseLeave;
+				} else {
 					Remove (vertical);
+					vertical.MouseEnter -= View_MouseEnter;
+					vertical.MouseLeave -= View_MouseLeave;
+				}
+				horizontal.Width = Dim.Fill (showVerticalScrollIndicator ? 1 : 0);
 			}
 		}
 
-		/// <summary>
-		/// This event is raised when the contents have scrolled
-		/// </summary>
-		//public event Action<ScrollView> Scrolled;
-		public override void Redraw(Rect region)
+		/// <inheritdoc cref="Redraw(Rect)"/>
+		public override void Redraw (Rect region)
 		{
-			SetViewsNeedsDisplay ();
 			Driver.SetAttribute (ColorScheme.Normal);
+			SetViewsNeedsDisplay ();
 			Clear ();
 
 			var savedClip = ClipToBounds ();
+			OnDrawContent (new Rect (ContentOffset,
+				new Size (Bounds.Width - (ShowVerticalScrollIndicator ? 1 : 0),
+					Bounds.Height - (ShowHorizontalScrollIndicator ? 1 : 0))));
 			contentView.Redraw (contentView.Bounds);
 			Driver.Clip = savedClip;
 
-			vertical.Redraw (vertical.Bounds);
-			horizontal.Redraw (horizontal.Bounds);
-			Driver.Clip = savedClip;
+			if (ShowVerticalScrollIndicator) {
+				vertical.Redraw (vertical.Bounds);
+			}
+
+			if (ShowHorizontalScrollIndicator) {
+				horizontal.Redraw (horizontal.Bounds);
+			}
+
+			// Fill in the bottom left corner
+			if (ShowVerticalScrollIndicator && ShowHorizontalScrollIndicator) {
+				AddRune (Bounds.Width - 1, Bounds.Height - 1, ' ');
+			}
 			Driver.SetAttribute (ColorScheme.Normal);
 		}
 
@@ -424,7 +504,7 @@ namespace Terminal.Gui {
 		}
 
 		///<inheritdoc cref="PositionCursor"/>
-		public override void PositionCursor()
+		public override void PositionCursor ()
 		{
 			if (InternalSubviews.Count == 0)
 				Driver.Move (0, 0);
@@ -490,7 +570,7 @@ namespace Terminal.Gui {
 		}
 
 		///<inheritdoc cref="ProcessKey"/>
-		public override bool ProcessKey(KeyEvent kb)
+		public override bool ProcessKey (KeyEvent kb)
 		{
 			if (base.ProcessKey (kb))
 				return true;

+ 10 - 0
UICatalog/Scenarios/Buttons.cs

@@ -118,6 +118,16 @@ namespace UICatalog {
 			};
 			Win.Add (moveBtn);
 
+			// Demonstrates how changing the View.Frame property can SIZE Views (#583)
+			y += 2;
+			var sizeBtn = new Button (10, y, "Size This Button via Frame") {
+				ColorScheme = Colors.Error,
+			};
+			moveBtn.Clicked = () => {
+				sizeBtn.Frame = new Rect (sizeBtn.Frame.X, sizeBtn.Frame.Y, sizeBtn.Frame.Width + 5, sizeBtn.Frame.Height);
+			};
+			Win.Add (sizeBtn);
+
 			// Demo changing hotkey
 			ustring MoveHotkey (ustring txt)
 			{

+ 123 - 0
UICatalog/Scenarios/CharacterMap.cs

@@ -0,0 +1,123 @@
+using NStack;
+using System.Collections.Generic;
+using System.Text;
+using Terminal.Gui;
+
+namespace UICatalog {
+	/// <summary>
+	/// This Scenario demonstrates building a custom control (a class deriving from View) that:
+	///   - Provides a simple "Character Map" application (like Windows' charmap.exe).
+	///   - Helps test unicode character rendering in Terminal.Gui
+	///   - Illustrates how to use ScrollView to do infinite scrolling
+	/// </summary>
+	[ScenarioMetadata (Name: "Character Map", Description: "Illustrates a custom control and Unicode")]
+	[ScenarioCategory ("Text")]
+	[ScenarioCategory ("Controls")]
+	class CharacterMap : Scenario {
+		public override void Setup ()
+		{
+			var charMap = new CharMap () { X = 0, Y = 0, Width = CharMap.RowWidth + 2, Height = Dim.Fill(), Start = 0x2500, 
+				ColorScheme = Colors.Dialog};
+
+			Win.Add (charMap);
+
+			Button CreateBlock(Window win, ustring title, int start, int end, View align)
+			{
+				var button = new Button ($"{title} (U+{start:x5}-{end:x5})") {
+					X = Pos.X (align),
+					Y = Pos.Bottom (align),
+					Clicked = () => {
+						charMap.Start = start;
+					},
+				};
+				win.Add (button);
+				return button;
+			};
+
+			var label = new Label ("Unicode Blocks:") { X = Pos.Right (charMap) + 2, Y = Pos.Y (charMap) };
+			Win.Add (label);
+			var button = CreateBlock (Win, "Currency Symbols", 0x20A0, 0x20CF, label);
+			button = CreateBlock (Win, "Letterlike Symbols", 0x2100, 0x214F, button);
+			button = CreateBlock (Win, "Arrows", 0x2190, 0x21ff, button);
+			button = CreateBlock (Win, "Mathematical symbols", 0x2200, 0x22ff, button);
+			button = CreateBlock (Win, "Miscellaneous Technical", 0x2300, 0x23ff, button);
+			button = CreateBlock (Win, "Box Drawing & Geometric Shapes", 0x2500, 0x25ff, button);
+			button = CreateBlock (Win, "Miscellaneous Symbols", 0x2600, 0x26ff, button);
+			button = CreateBlock (Win, "Dingbats", 0x2700, 0x27ff, button);
+			button = CreateBlock (Win, "Braille", 0x2800, 0x28ff, button);
+			button = CreateBlock (Win, "Miscellaneous Symbols and Arrows", 0x2b00, 0x2bff, button);
+			button = CreateBlock (Win, "Alphabetic Presentation Forms", 0xFB00, 0xFb4f, button);
+			button = CreateBlock (Win, "Cuneiform Numbers and Punctuation[1", 0x12400, 0x1240f, button);
+			button = CreateBlock (Win, "Chess Symbols", 0x1FA00, 0x1FA0f, button);
+			button = CreateBlock (Win, "End", CharMap.MaxCodePointVal - 16, CharMap.MaxCodePointVal, button);
+		}
+	}
+
+	class CharMap : ScrollView {
+
+		/// <summary>
+		/// Specifies the starting offset for the character map. The default is 0x2500 
+		/// which is the Box Drawing characters.
+		/// </summary>
+		public int Start {
+			get => _start;
+			set {
+				_start = value;
+				ContentOffset = new Point (0, _start / 16);
+
+				SetNeedsDisplay ();
+			}
+		}
+		int _start = 0x2500;
+
+		public static int MaxCodePointVal => 0xE0FFF;
+
+		// Row Header + space + (space + char + space)
+		public static int RowHeaderWidth => $"U+{MaxCodePointVal:x5}".Length;
+		public static int RowWidth => RowHeaderWidth + 1 + (" c ".Length * 16);
+
+		public CharMap ()
+		{
+			ContentSize = new Size (CharMap.RowWidth, MaxCodePointVal / 16);
+			ShowVerticalScrollIndicator = true;
+			ShowHorizontalScrollIndicator = false;
+			LayoutComplete += (sender, args) => {
+				if (Bounds.Width <= RowWidth) {
+					ShowHorizontalScrollIndicator = true;
+				} else {
+					ShowHorizontalScrollIndicator = false;
+				}
+			};
+
+			DrawContent += CharMap_DrawContent;
+		}
+
+#if true
+		private void CharMap_DrawContent (object sender, Rect viewport)
+		{
+			for (int header = 0; header < 16; header++) {
+				Move (viewport.X + RowHeaderWidth + 1 + (header * 3), 0);
+				Driver.AddStr ($" {header:x} ");
+			}
+			for (int row = 0; row < viewport.Height - 1; row++) {
+				int val = (-viewport.Y + row) * 16;
+				if (val < MaxCodePointVal) {
+					var rowLabel = $"U+{val / 16:x4}x";
+					Move (0, row + 1);
+					Driver.AddStr (rowLabel);
+					for (int col = 0; col < 16; col++) {
+						Move (viewport.X + RowHeaderWidth + 1 + (col * 3), 0 + row + 1);
+						Driver.AddStr ($" {(char)((-viewport.Y + row) * 16 + col)} ");
+					}
+				}
+			}
+		}
+#else
+		public override void OnDrawContent (Rect viewport)
+		{
+			CharMap_DrawContent(this, viewport);
+			base.OnDrawContent (viewport);
+		}
+#endif
+	}
+}

+ 27 - 8
UICatalog/Scenarios/Scrolling.cs

@@ -95,12 +95,13 @@ namespace UICatalog {
 			Win.Add (label);
 
 			// BUGBUG: ScrollView only supports Absolute Positioning (#72)
-			var scrollView = new ScrollView (new Rect (2, 2, 50, 20));
-			scrollView.ColorScheme = Colors.TopLevel;
-			scrollView.ContentSize = new Size (200, 100);
-			//ContentOffset = new Point (0, 0),
-			scrollView.ShowVerticalScrollIndicator = true;
-			scrollView.ShowHorizontalScrollIndicator = true;
+			var scrollView = new ScrollView (new Rect (2, 2, 50, 20)) {
+				ColorScheme = Colors.TopLevel,
+				ContentSize = new Size (200, 100),
+				//ContentOffset = new Point (0, 0),
+				ShowVerticalScrollIndicator = true,
+				ShowHorizontalScrollIndicator = true,
+			};
 
 			const string rule = "|123456789";
 			var horizontalRuler = new Label ("") {
@@ -177,13 +178,31 @@ namespace UICatalog {
 			};
 			scrollView.Add (anchorButton);
 
+			var hCheckBox = new CheckBox ("Horizontal Scrollbar", scrollView.ShowHorizontalScrollIndicator) {
+				X = Pos.X(scrollView),
+				Y = Pos.Bottom(scrollView) + 1,
+			};
+			hCheckBox.Toggled += (sender, previousChecked) => {
+				scrollView.ShowHorizontalScrollIndicator = ((CheckBox)sender).Checked;
+			};
+			Win.Add (hCheckBox);
+
+			var vCheckBox = new CheckBox ("Vertical Scrollbar", scrollView.ShowVerticalScrollIndicator) {
+				X = Pos.Right (hCheckBox) + 3,
+				Y = Pos.Bottom (scrollView) + 1,
+			};
+			vCheckBox.Toggled += (sender, previousChecked) => {
+				scrollView.ShowVerticalScrollIndicator = ((CheckBox)sender).Checked;
+			};
+			Win.Add (vCheckBox);
+
 			var scrollView2 = new ScrollView (new Rect (55, 2, 20, 8)) {
 				ContentSize = new Size (20, 50),
 				//ContentOffset = new Point (0, 0),
 				ShowVerticalScrollIndicator = true,
 				ShowHorizontalScrollIndicator = true
 			};
-			scrollView2.Add (new Filler(new Rect (0, 0, 60, 40)));
+			scrollView2.Add (new Filler (new Rect (0, 0, 60, 40)));
 
 			// This is just to debug the visuals of the scrollview when small
 			var scrollView3 = new ScrollView (new Rect (55, 15, 3, 3)) {
@@ -205,7 +224,7 @@ namespace UICatalog {
 
 			var progress = new ProgressBar ();
 			progress.X = 5;
-			progress.Y = Pos.AnchorEnd (3);
+			progress.Y = Pos.AnchorEnd (2);
 			progress.Width = 50;
 			bool timer (MainLoop caller)
 			{

+ 1 - 1
UICatalog/Scenarios/TextAlignment.cs → UICatalog/Scenarios/TextAlignments.cs

@@ -6,7 +6,7 @@ using Terminal.Gui;
 namespace UICatalog {
 	[ScenarioMetadata (Name: "Text Alignment", Description: "Demonstrates text alignment")]
 	[ScenarioCategory ("Text")]
-	class TextAlignment : Scenario {
+	class TextAlignments : Scenario {
 		public override void Setup ()
 		{
 			int i = 1;

+ 2 - 9
UICatalog/Scenarios/Unicode.cs

@@ -10,8 +10,6 @@ namespace UICatalog {
 	class UnicodeInMenu : Scenario {
 		public override void Setup ()
 		{
-			const int margin = 1;
-
 			var menu = new MenuBar (new MenuBarItem [] {
 				new MenuBarItem ("_Файл", new MenuItem [] {
 					new MenuItem ("_Создать", "Creates new file", null),
@@ -27,14 +25,9 @@ namespace UICatalog {
 			});
 			Top.Add (menu);
 
-			var label = new Label ("Button:") { X = margin, Y = margin };
-			Win.Add (label);
-			var button = new Button (" ~  s  gui.cs   master ↑10") { X = 15, Y = Pos.Y (label) };
-			Win.Add (button);
-
-			label = new Label ("Button:") { X = Pos.X (label), Y = Pos.Bottom (label) + 1 };
+			var label = new Label ("Button:") { X = 0, Y = 1 };
 			Win.Add (label);
-			var button2 = new Button ("Со_хранить") { X = 15, Y = Pos.Y (label), Width = Dim.Percent (50) };
+			var button2 = new Button ("Со_хранить") { X = 15, Y = Pos.Y (label), Width = Dim.Percent (50), };
 			Win.Add (button2);
 
 			label = new Label ("CheckBox:") { X = Pos.X (label), Y = Pos.Bottom (label) + 1 };