Browse Source

Refactored and went back and forth. Things are working well. Tests not so much

Tig 9 months ago
parent
commit
f7e0a293c5

+ 2 - 0
Terminal.Gui/View/View.ScrollBars.cs

@@ -130,11 +130,13 @@ public partial class View
         {
         {
             if (_verticalScrollBar.IsValueCreated)
             if (_verticalScrollBar.IsValueCreated)
             {
             {
+                _verticalScrollBar.Value.ViewportDimension = Viewport.Height;
                 _verticalScrollBar.Value.ContentPosition = Viewport.Y;
                 _verticalScrollBar.Value.ContentPosition = Viewport.Y;
             }
             }
 
 
             if (_horizontalScrollBar.IsValueCreated)
             if (_horizontalScrollBar.IsValueCreated)
             {
             {
+                _horizontalScrollBar.Value.ViewportDimension = Viewport.Width;
                 _horizontalScrollBar.Value.ContentPosition = Viewport.X;
                 _horizontalScrollBar.Value.ContentPosition = Viewport.X;
             }
             }
         };
         };

+ 77 - 60
Terminal.Gui/Views/Scroll/Scroll.cs

@@ -24,8 +24,9 @@ public class Scroll : View, IOrientation, IDesignable
     public Scroll ()
     public Scroll ()
     {
     {
         _slider = new ();
         _slider = new ();
-        Add (_slider);
-        _slider.FrameChanged += OnSliderOnFrameChanged;
+        base.Add (_slider);
+        _slider.Scroll += SliderOnScroll;
+        _slider.PositionChanged += SliderOnPositionChanged;
 
 
         CanFocus = false;
         CanFocus = false;
 
 
@@ -38,6 +39,7 @@ public class Scroll : View, IOrientation, IDesignable
         OnOrientationChanged (Orientation);
         OnOrientationChanged (Orientation);
     }
     }
 
 
+
     /// <inheritdoc/>
     /// <inheritdoc/>
     protected override void OnSubviewLayout (LayoutEventArgs args)
     protected override void OnSubviewLayout (LayoutEventArgs args)
     {
     {
@@ -104,12 +106,32 @@ public class Scroll : View, IOrientation, IDesignable
         set => _slider.ShowPercent = value;
         set => _slider.ShowPercent = value;
     }
     }
 
 
-    private int ViewportDimension => Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
+    private int? _viewportDimension;
+
+    /// <summary>
+    ///     Gets or sets the size of the viewport into the content being scrolled, bounded by <see cref="Size"/>.
+    /// </summary>
+    /// <remarks>
+    ///     If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
+    /// </remarks>
+    public int ViewportDimension
+    {
+        get
+        {
+            if (_viewportDimension.HasValue)
+            {
+                return _viewportDimension.Value;
+            }
+            return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
+
+        }
+        set => _viewportDimension = value;
+    }
 
 
     private int _size;
     private int _size;
 
 
     /// <summary>
     /// <summary>
-    ///     Gets or sets the total size of the content that can be scrolled.
+    ///     Gets or sets the size of the content that can be scrolled.
     /// </summary>
     /// </summary>
     public int Size
     public int Size
     {
     {
@@ -135,43 +157,37 @@ public class Scroll : View, IOrientation, IDesignable
     public event EventHandler<EventArgs<int>>? SizeChanged;
     public event EventHandler<EventArgs<int>>? SizeChanged;
 
 
     #region SliderPosition
     #region SliderPosition
-    private void OnSliderOnFrameChanged (object? sender, EventArgs<Rectangle> args)
+
+    private void SliderOnPositionChanged (object? sender, EventArgs<int> e)
     {
     {
         if (ViewportDimension == 0)
         if (ViewportDimension == 0)
         {
         {
             return;
             return;
         }
         }
 
 
-        int framePos = Orientation == Orientation.Vertical ? args.CurrentValue.Y : args.CurrentValue.X;
+        int calculatedSliderPos = CalculateSliderPosition (_contentPosition);
 
 
-        RaiseSliderPositionChangeEvents (CalculateSliderPosition (_contentPosition), framePos);
-    }
+        ContentPosition = (int)Math.Round ((double)e.CurrentValue / (ViewportDimension - _slider.Size) * (Size - ViewportDimension));
 
 
-    /// <summary>
-    ///     Gets or sets the position of the start of the Scroll slider, within the Viewport.
-    /// </summary>
-    public int SliderPosition
-    {
-        get => CalculateSliderPosition (_contentPosition);
-        set => RaiseSliderPositionChangeEvents (_slider.Position, value);
+        RaiseSliderPositionChangeEvents (calculatedSliderPos, e.CurrentValue);
     }
     }
 
 
-    private void RaiseSliderPositionChangeEvents (int currentSliderPosition, int newSliderPosition)
+    private void SliderOnScroll (object? sender, EventArgs<int> e)
     {
     {
-        if (/*newSliderPosition > Size - ViewportDimension ||*/ currentSliderPosition == newSliderPosition)
-        {
-            return;
-        }
-
-        if (OnSliderPositionChanging (currentSliderPosition, newSliderPosition))
+        if (ViewportDimension == 0)
         {
         {
             return;
             return;
         }
         }
+    }
 
 
-        CancelEventArgs<int> args = new (ref currentSliderPosition, ref newSliderPosition);
-        SliderPositionChanging?.Invoke (this, args);
+    /// <summary>
+    ///     Gets or sets the position of the start of the Scroll slider, within the Viewport.
+    /// </summary>
+    public int GetSliderPosition () => CalculateSliderPosition (_contentPosition);
 
 
-        if (args.Cancel)
+    private void RaiseSliderPositionChangeEvents (int calculatedSliderPosition, int newSliderPosition)
+    {
+        if (/*newSliderPosition > Size - ViewportDimension ||*/ calculatedSliderPosition == newSliderPosition)
         {
         {
             return;
             return;
         }
         }
@@ -179,27 +195,14 @@ public class Scroll : View, IOrientation, IDesignable
         // This sets the slider position and clamps the value
         // This sets the slider position and clamps the value
         _slider.Position = newSliderPosition;
         _slider.Position = newSliderPosition;
 
 
-        ContentPosition = (int)Math.Round ((double)newSliderPosition / (ViewportDimension - _slider.Size) * (Size - ViewportDimension));
-
         OnSliderPositionChanged (newSliderPosition);
         OnSliderPositionChanged (newSliderPosition);
         SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
         SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
     }
     }
 
 
-    /// <summary>
-    ///     Called when <see cref="SliderPosition"/> is changing. Return true to cancel the change.
-    /// </summary>
-    protected virtual bool OnSliderPositionChanging (int currentSliderPosition, int newSliderPosition) { return false; }
-
-    /// <summary>
-    ///     Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
-    ///     <see langword="true"/> to prevent the position from being changed.
-    /// </summary>
-    public event EventHandler<CancelEventArgs<int>>? SliderPositionChanging;
-
-    /// <summary>Called when <see cref="SliderPosition"/> has changed.</summary>
+    /// <summary>Called when the slider position has changed.</summary>
     protected virtual void OnSliderPositionChanged (int position) { }
     protected virtual void OnSliderPositionChanged (int position) { }
 
 
-    /// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
+    /// <summary>Raised when the slider position has changed.</summary>
     public event EventHandler<EventArgs<int>>? SliderPositionChanged;
     public event EventHandler<EventArgs<int>>? SliderPositionChanged;
 
 
     private int CalculateSliderPosition (int contentPosition)
     private int CalculateSliderPosition (int contentPosition)
@@ -219,8 +222,17 @@ public class Scroll : View, IOrientation, IDesignable
     private int _contentPosition;
     private int _contentPosition;
 
 
     /// <summary>
     /// <summary>
-    ///     Gets or sets the position of the ScrollSlider within the range of 0...<see cref="Size"/>.
+    ///     Gets or sets the position of the slider relative to <see cref="Size"/>.
     /// </summary>
     /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         The content position is clamped to 0 and <see cref="Size"/> minus <see cref="ViewportDimension"/>.
+    ///     </para>
+    ///     <para>
+    ///         Setting will result in the <see cref="ContentPositionChanging"/> and <see cref="ContentPositionChanged"/>
+    ///         events being raised.
+    ///     </para>
+    /// </remarks>
     public int ContentPosition
     public int ContentPosition
     {
     {
         get => _contentPosition;
         get => _contentPosition;
@@ -231,14 +243,17 @@ public class Scroll : View, IOrientation, IDesignable
                 return;
                 return;
             }
             }
 
 
-            RaiseContentPositionChangeEvents (value);
+            // Clamp the value between 0 and Size - ViewportDimension
+            int newContentPosition = (int)Math.Clamp (value, 0, Math.Max (0, Size - ViewportDimension));
+
+            RaiseContentPositionChangeEvents (newContentPosition);
+
+            _slider.SetPosition (CalculateSliderPosition (_contentPosition));
         }
         }
     }
     }
 
 
     private void RaiseContentPositionChangeEvents (int newContentPosition)
     private void RaiseContentPositionChangeEvents (int newContentPosition)
     {
     {
-        // Clamp the value between 0 and Size - ViewportDimension
-        newContentPosition = (int)Math.Clamp (newContentPosition, 0, Math.Max (0, Size - ViewportDimension));
 
 
         if (OnContentPositionChanging (_contentPosition, newContentPosition))
         if (OnContentPositionChanging (_contentPosition, newContentPosition))
         {
         {
@@ -255,8 +270,6 @@ public class Scroll : View, IOrientation, IDesignable
 
 
         _contentPosition = newContentPosition;
         _contentPosition = newContentPosition;
 
 
-        SliderPosition = CalculateSliderPosition (_contentPosition);
-
         OnContentPositionChanged (_contentPosition);
         OnContentPositionChanged (_contentPosition);
         ContentPositionChanged?.Invoke (this, new (in _contentPosition));
         ContentPositionChanged?.Invoke (this, new (in _contentPosition));
     }
     }
@@ -291,35 +304,39 @@ public class Scroll : View, IOrientation, IDesignable
     /// <inheritdoc/>
     /// <inheritdoc/>
     protected override bool OnMouseClick (MouseEventArgs args)
     protected override bool OnMouseClick (MouseEventArgs args)
     {
     {
+        // Check if the mouse click is a single click
         if (!args.IsSingleClicked)
         if (!args.IsSingleClicked)
         {
         {
             return false;
             return false;
         }
         }
 
 
+        int sliderCenter;
+        int distanceFromCenter;
+
         if (Orientation == Orientation.Vertical)
         if (Orientation == Orientation.Vertical)
         {
         {
-            // If the position is w/in the slider frame ignore
-            if (args.Position.Y >= _slider.Frame.Y && args.Position.Y < _slider.Frame.Y + _slider.Frame.Height)
-            {
-                return false;
-            }
-
-            SliderPosition = args.Position.Y;
+            sliderCenter = _slider.Frame.Y + _slider.Frame.Height / 2;
+            distanceFromCenter = args.Position.Y - sliderCenter;
         }
         }
         else
         else
         {
         {
-            // If the position is w/in the slider frame ignore
-            if (args.Position.X >= _slider.Frame.X && args.Position.X < _slider.Frame.X + _slider.Frame.Width)
-            {
-                return false;
-            }
-
-            SliderPosition = args.Position.X;
+            sliderCenter = _slider.Frame.X + _slider.Frame.Width / 2;
+            distanceFromCenter = args.Position.X - sliderCenter;
         }
         }
 
 
+        // Ratio of the distance to the viewport dimension
+        double ratio = (double)Math.Abs (distanceFromCenter) / ViewportDimension;
+        // Jump size based on the ratio and the total content size
+        int jump = (int)Math.Ceiling (ratio * Size);
+
+        // Adjust the content position based on the distance
+        ContentPosition += distanceFromCenter < 0 ? -jump : jump;
+
         return true;
         return true;
     }
     }
 
 
+
+
     /// <summary>
     /// <summary>
     ///     Gets or sets the amount each mouse hweel event will incremenet/decrement the <see cref="ContentPosition"/>.
     ///     Gets or sets the amount each mouse hweel event will incremenet/decrement the <see cref="ContentPosition"/>.
     /// </summary>
     /// </summary>

+ 21 - 32
Terminal.Gui/Views/Scroll/ScrollBar.cs

@@ -11,9 +11,6 @@ namespace Terminal.Gui;
 ///     and clicking with the mouse to scroll.
 ///     and clicking with the mouse to scroll.
 /// </summary>
 /// </summary>
 /// <remarks>
 /// <remarks>
-///     <para>
-///         <see cref="SliderPosition"/> indicates the number of rows or columns the Scroll has moved from 0.
-///     </para>
 /// </remarks>
 /// </remarks>
 public class ScrollBar : View, IOrientation, IDesignable
 public class ScrollBar : View, IOrientation, IDesignable
 {
 {
@@ -27,7 +24,6 @@ public class ScrollBar : View, IOrientation, IDesignable
         CanFocus = false;
         CanFocus = false;
 
 
         _scroll = new ();
         _scroll = new ();
-        _scroll.SliderPositionChanging += OnScrollOnSliderPositionChanging;
         _scroll.SliderPositionChanged += OnScrollOnSliderPositionChanged;
         _scroll.SliderPositionChanged += OnScrollOnSliderPositionChanged;
         _scroll.ContentPositionChanging += OnScrollOnContentPositionChanging;
         _scroll.ContentPositionChanging += OnScrollOnContentPositionChanging;
         _scroll.ContentPositionChanged += OnScrollOnContentPositionChanged;
         _scroll.ContentPositionChanged += OnScrollOnContentPositionChanged;
@@ -112,6 +108,8 @@ public class ScrollBar : View, IOrientation, IDesignable
             Height = 1;
             Height = 1;
         }
         }
 
 
+        // Force a layout to ensure _scroll 
+        Layout ();
         _scroll.Orientation = newOrientation;
         _scroll.Orientation = newOrientation;
     }
     }
 
 
@@ -183,43 +181,34 @@ public class ScrollBar : View, IOrientation, IDesignable
     }
     }
 
 
     /// <summary>Gets or sets the position of the slider within the ScrollBar's Viewport.</summary>
     /// <summary>Gets or sets the position of the slider within the ScrollBar's Viewport.</summary>
-    /// <value>The position.</value>
-    public int SliderPosition
-    {
-        get => _scroll.SliderPosition;
-        set => _scroll.SliderPosition = value;
-    }
+    /// <returns>The position.</returns>
+    public int GetSliderPosition () => _scroll.GetSliderPosition ();
 
 
-    private void OnScrollOnSliderPositionChanging (object? sender, CancelEventArgs<int> e) { SliderPositionChanging?.Invoke (this, e); }
     private void OnScrollOnSliderPositionChanged (object? sender, EventArgs<int> e) { SliderPositionChanged?.Invoke (this, e); }
     private void OnScrollOnSliderPositionChanged (object? sender, EventArgs<int> e) { SliderPositionChanged?.Invoke (this, e); }
 
 
-    /// <summary>
-    ///     Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
-    ///     <see langword="true"/> to prevent the position from being changed.
-    /// </summary>
-    public event EventHandler<CancelEventArgs<int>>? SliderPositionChanging;
-
-    /// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
+    /// <summary>Raised when the position of the slider has changed.</summary>
     public event EventHandler<EventArgs<int>>? SliderPositionChanged;
     public event EventHandler<EventArgs<int>>? SliderPositionChanged;
 
 
-
     /// <summary>
     /// <summary>
     ///     Gets or sets the size of the Scroll. This is the total size of the content that can be scrolled through.
     ///     Gets or sets the size of the Scroll. This is the total size of the content that can be scrolled through.
     /// </summary>
     /// </summary>
     public int Size
     public int Size
     {
     {
-        get
-        {
-            // Add two for increment/decrement buttons
-            return _scroll.Size + 2;
-        }
-        set
-        {
-            // Remove two for increment/decrement buttons
-            _scroll.Size = value - 2;
-        }
+        get => _scroll.Size;
+        set => _scroll.Size = value;
     }
     }
 
 
+    /// <summary>
+    ///     Gets or sets the size of the viewport into the content being scrolled, bounded by <see cref="Size"/>.
+    /// </summary>
+    /// <remarks>
+    ///     If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
+    /// </remarks>
+    public int ViewportDimension
+    {
+        get => _scroll.ViewportDimension;
+        set => _scroll.ViewportDimension = value;
+    }
     /// <summary>
     /// <summary>
     ///     Gets or sets the position of the ScrollSlider within the range of 0...<see cref="Size"/>.
     ///     Gets or sets the position of the ScrollSlider within the range of 0...<see cref="Size"/>.
     /// </summary>
     /// </summary>
@@ -233,12 +222,12 @@ public class ScrollBar : View, IOrientation, IDesignable
     private void OnScrollOnContentPositionChanged (object? sender, EventArgs<int> e) { ContentPositionChanged?.Invoke (this, e); }
     private void OnScrollOnContentPositionChanged (object? sender, EventArgs<int> e) { ContentPositionChanged?.Invoke (this, e); }
 
 
     /// <summary>
     /// <summary>
-    ///     Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
+    ///     Raised when the <see cref="ContentPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
     ///     <see langword="true"/> to prevent the position from being changed.
     ///     <see langword="true"/> to prevent the position from being changed.
     /// </summary>
     /// </summary>
     public event EventHandler<CancelEventArgs<int>>? ContentPositionChanging;
     public event EventHandler<CancelEventArgs<int>>? ContentPositionChanging;
 
 
-    /// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
+    /// <summary>Raised when the <see cref="ContentPosition"/> has changed.</summary>
     public event EventHandler<EventArgs<int>>? ContentPositionChanged;
     public event EventHandler<EventArgs<int>>? ContentPositionChanged;
 
 
     /// <summary>Raised when <see cref="Size"/> has changed.</summary>
     /// <summary>Raised when <see cref="Size"/> has changed.</summary>
@@ -320,7 +309,7 @@ public class ScrollBar : View, IOrientation, IDesignable
         Width = 1;
         Width = 1;
         Height = Dim.Fill ();
         Height = Dim.Fill ();
         Size = 200;
         Size = 200;
-        SliderPosition = 10;
+        ContentPosition = 10;
         //ShowPercent = true;
         //ShowPercent = true;
         return true;
         return true;
     }
     }

+ 114 - 25
Terminal.Gui/Views/Scroll/ScrollSlider.cs

@@ -1,5 +1,6 @@
 #nullable enable
 #nullable enable
 
 
+using System.ComponentModel;
 using System.Diagnostics;
 using System.Diagnostics;
 
 
 namespace Terminal.Gui;
 namespace Terminal.Gui;
@@ -39,6 +40,8 @@ public class ScrollSlider : View, IOrientation, IDesignable
 
 
         // Default size is 1
         // Default size is 1
         Size = 1;
         Size = 1;
+
+        FrameChanged += OnFrameChanged;
     }
     }
 
 
     #region IOrientation members
     #region IOrientation members
@@ -67,6 +70,7 @@ public class ScrollSlider : View, IOrientation, IDesignable
         // Reset Position to 0 when changing orientation
         // Reset Position to 0 when changing orientation
         X = 0;
         X = 0;
         Y = 0;
         Y = 0;
+        //Position = 0;
 
 
         // Reset Size to 1 when changing orientation
         // Reset Size to 1 when changing orientation
         if (Orientation == Orientation.Vertical)
         if (Orientation == Orientation.Vertical)
@@ -103,7 +107,7 @@ public class ScrollSlider : View, IOrientation, IDesignable
         set
         set
         {
         {
             _showPercent = value;
             _showPercent = value;
-            SetNeedsDraw();
+            SetNeedsDraw ();
         }
         }
     }
     }
 
 
@@ -123,11 +127,11 @@ public class ScrollSlider : View, IOrientation, IDesignable
         {
         {
             if (Orientation == Orientation.Vertical)
             if (Orientation == Orientation.Vertical)
             {
             {
-                return Frame.Height;
+                return Viewport.Height;
             }
             }
             else
             else
             {
             {
-                return Frame.Width;
+                return Viewport.Width;
             }
             }
         }
         }
         set
         set
@@ -148,38 +152,123 @@ public class ScrollSlider : View, IOrientation, IDesignable
     }
     }
 
 
     /// <summary>
     /// <summary>
-    ///     Gets or sets the position of the ScrollSlider relative to the size of the ScrollSlider's Frame. This is a helper that simply gets or sets the X or Y depending on the
-    ///     <see cref="Orientation"/>. The position will be constrained such that the ScrollSlider will not go outside the Viewport of
+    ///     Gets the size of the viewport into the content being scrolled, bounded by <see cref="Size"/>.
+    /// </summary>
+    /// <remarks>
+    ///     This is the SuperView's Viewport demension.
+    /// </remarks>
+    public int ViewportDimension => Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 0 : SuperView?.Viewport.Width ?? 0;
+
+    private void OnFrameChanged (object? sender, EventArgs<Rectangle> e)
+    {
+        Position = Orientation == Orientation.Vertical ? e.CurrentValue.Y : e.CurrentValue.X;
+    }
+
+    private int _position;
+
+    /// <summary>
+    ///     Gets or sets the position of the ScrollSlider relative to the size of the ScrollSlider's Frame.
+    ///     The position will be constrained such that the ScrollSlider will not go outside the Viewport of
     ///     the <see cref="View.SuperView"/>.
     ///     the <see cref="View.SuperView"/>.
     /// </summary>
     /// </summary>
     public int Position
     public int Position
     {
     {
-        get
+        get => _position;
+        set
         {
         {
-            if (Orientation == Orientation.Vertical)
+            if (_position == value)
             {
             {
-                return Frame.Y;
-            }
-            else
-            {
-                return Frame.X;
+                return;
             }
             }
+
+            RaisePositionChangeEvents (ClampPosition (value));
+
+            SetNeedsLayout ();
         }
         }
-        set
+    }
+
+    public void SetPosition (int position)
+    {
+        _position = ClampPosition (position);
+
+        if (Orientation == Orientation.Vertical)
         {
         {
-            if (Orientation == Orientation.Vertical)
-            {
-                int viewport = Math.Max (1, SuperView?.Viewport.Height ?? 1);
-                Y = Math.Clamp (value, 0, viewport - Frame.Height);
-            }
-            else
-            {
-                int viewport = Math.Max (1, SuperView?.Viewport.Width ?? 1);
-                X = Math.Clamp (value, 0, viewport - Frame.Width);
-            }
+            Y = _position;
+        }
+        else
+        {
+            X = _position;
         }
         }
     }
     }
 
 
+    private int ClampPosition (int newPosittion)
+    {
+        if (SuperView is null || !IsInitialized)
+        {
+            return 1;
+        }
+
+        if (Orientation == Orientation.Vertical)
+        {
+            return Math.Clamp (newPosittion, 0, ViewportDimension - Viewport.Height);
+        }
+        else
+        {
+            return Math.Clamp (newPosittion, 0, ViewportDimension - Viewport.Width);
+        }
+    }
+
+    private void RaisePositionChangeEvents (int newPosition)
+    {
+        if (OnPositionChanging (_position, newPosition))
+        {
+            return;
+        }
+
+        CancelEventArgs<int> args = new (ref _position, ref newPosition);
+        PositionChanging?.Invoke (this, args);
+
+        if (args.Cancel)
+        {
+            return;
+        }
+
+        int scrollAmount = newPosition -_position;
+        _position = newPosition;
+
+        OnPositionChanged (_position);
+        PositionChanged?.Invoke (this, new (in _position));
+
+        OnScroll (scrollAmount);
+        Scroll?.Invoke (this, new (in scrollAmount));
+
+        RaiseSelecting (new CommandContext (Command.Select, null, null, scrollAmount));
+    }
+
+    /// <summary>
+    ///     Called when <see cref="Position"/> is changing. Return true to cancel the change.
+    /// </summary>
+    protected virtual bool OnPositionChanging (int currentPos, int newPos) { return false; }
+
+    /// <summary>
+    ///     Raised when the <see cref="Position"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
+    ///     <see langword="true"/> to prevent the position from being changed.
+    /// </summary>
+    public event EventHandler<CancelEventArgs<int>>? PositionChanging;
+
+    /// <summary>Called when <see cref="Position"/> has changed.</summary>
+    protected virtual void OnPositionChanged (int position) { }
+
+    /// <summary>Raised when the <see cref="Position"/> has changed.</summary>
+    public event EventHandler<EventArgs<int>>? PositionChanged;
+
+
+    /// <summary>Called when <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
+    protected virtual void OnScroll (int scrollAmount) { }
+
+    /// <summary>Raised when the <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
+    public event EventHandler<EventArgs<int>>? Scroll;
+
     /// <inheritdoc/>
     /// <inheritdoc/>
     protected override bool OnDrawingText ()
     protected override bool OnDrawingText ()
     {
     {
@@ -240,7 +329,7 @@ public class ScrollSlider : View, IOrientation, IDesignable
                 if (Orientation == Orientation.Vertical)
                 if (Orientation == Orientation.Vertical)
                 {
                 {
                     Y = Frame.Y + offset < 0
                     Y = Frame.Y + offset < 0
-                            ? 0
+                                  ? 0
                             : Frame.Y + offset + Frame.Height > superViewDimension
                             : Frame.Y + offset + Frame.Height > superViewDimension
                                 ? Math.Max (superViewDimension - Frame.Height, 0)
                                 ? Math.Max (superViewDimension - Frame.Height, 0)
                                 : Frame.Y + offset;
                                 : Frame.Y + offset;
@@ -248,7 +337,7 @@ public class ScrollSlider : View, IOrientation, IDesignable
                 else
                 else
                 {
                 {
                     X = Frame.X + offset < 0
                     X = Frame.X + offset < 0
-                            ? 0
+                                  ? 0
                             : Frame.X + offset + Frame.Width > superViewDimension
                             : Frame.X + offset + Frame.Width > superViewDimension
                                 ? Math.Max (superViewDimension - Frame.Width, 0)
                                 ? Math.Max (superViewDimension - Frame.Width, 0)
                                 : Frame.X + offset;
                                 : Frame.X + offset;

+ 180 - 572
UICatalog/Scenarios/CharacterMap.cs → UICatalog/Scenarios/CharacterMap/CharMap.cs

@@ -1,353 +1,38 @@
-#define OTHER_CONTROLS
-
+#nullable enable
 using System;
 using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http;
-using System.Reflection;
 using System.Text;
 using System.Text;
 using System.Text.Json;
 using System.Text.Json;
-using System.Text.Unicode;
-using System.Threading.Tasks;
 using Terminal.Gui;
 using Terminal.Gui;
-using static Terminal.Gui.SpinnerStyle;
 
 
 namespace UICatalog.Scenarios;
 namespace UICatalog.Scenarios;
 
 
 /// <summary>
 /// <summary>
-///     This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
-///     "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
-///     Illustrates how to do infinite scrolling
+///     A scrollable map of the Unicode codepoints.
 /// </summary>
 /// </summary>
-[ScenarioMetadata ("Character Map", "Unicode viewer demonstrating infinite content, scrolling, and Unicode.")]
-[ScenarioCategory ("Text and Formatting")]
-[ScenarioCategory ("Drawing")]
-[ScenarioCategory ("Controls")]
-[ScenarioCategory ("Layout")]
-[ScenarioCategory ("Scrolling")]
-public class CharacterMap : Scenario
+/// <remarks>
+///     See <see href="CharacterMap/README.md"/> for details.
+/// </remarks>
+public class CharMap : View, IDesignable
 {
 {
-    public Label _errorLabel;
-    private TableView _categoryList;
-    private CharMap _charMap;
-
-    // Don't create a Window, just return the top-level view
-    public override void Main ()
-    {
-        Application.Init ();
-
-        var top = new Window
-        {
-            BorderStyle = LineStyle.None
-        };
-
-        _charMap = new ()
-        {
-            X = 0,
-            Y = 0,
-            Width = Dim.Fill (Dim.Func (() => _categoryList.Frame.Width)),
-            Height = Dim.Fill ()
-        };
-        top.Add (_charMap);
-
-#if OTHER_CONTROLS
-        _charMap.Y = 1;
-
-        var jumpLabel = new Label
-        {
-            X = Pos.Right (_charMap) + 1,
-            Y = Pos.Y (_charMap),
-            HotKeySpecifier = (Rune)'_',
-            Text = "_Jump To Code Point:"
-        };
-        top.Add (jumpLabel);
-
-        var jumpEdit = new TextField
-        {
-            X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3"
-        };
-        top.Add (jumpEdit);
-
-        _errorLabel = new ()
-        {
-            X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap), ColorScheme = Colors.ColorSchemes ["error"], Text = "err"
-        };
-        top.Add (_errorLabel);
-
-        jumpEdit.Accepting += JumpEditOnAccept;
-
-        _categoryList = new () { X = Pos.Right (_charMap), Y = Pos.Bottom (jumpLabel), Height = Dim.Fill () };
-        _categoryList.FullRowSelect = true;
-        _categoryList.MultiSelect = false;
-
-        //jumpList.Style.ShowHeaders = false;
-        //jumpList.Style.ShowHorizontalHeaderOverline = false;
-        //jumpList.Style.ShowHorizontalHeaderUnderline = false;
-        _categoryList.Style.ShowHorizontalBottomline = true;
-
-        //jumpList.Style.ShowVerticalCellLines = false;
-        //jumpList.Style.ShowVerticalHeaderLines = false;
-        _categoryList.Style.AlwaysShowHeaders = true;
-
-        var isDescending = false;
-
-        _categoryList.Table = CreateCategoryTable (0, isDescending);
-
-        // if user clicks the mouse in TableView
-        _categoryList.MouseClick += (s, e) =>
-                                    {
-                                        _categoryList.ScreenToCell (e.Position, out int? clickedCol);
-
-                                        if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked))
-                                        {
-                                            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-                                            string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
-                                            isDescending = !isDescending;
-
-                                            _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
-
-                                            table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-
-                                            _categoryList.SelectedRow = table.Data
-                                                                             .Select ((item, index) => new { item, index })
-                                                                             .FirstOrDefault (x => x.item.Category == prevSelection)
-                                                                             ?.index
-                                                                        ?? -1;
-                                        }
-                                    };
-
-        int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ());
-
-        _categoryList.Style.ColumnStyles.Add (
-                                              0,
-                                              new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
-                                             );
-        _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 });
-        _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 });
-
-        _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4;
-
-        _categoryList.SelectedCellChanged += (s, args) =>
-                                             {
-                                                 EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-                                                 _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start;
-                                             };
-
-        top.Add (_categoryList);
-
-        var menu = new MenuBar
-        {
-            Menus =
-            [
-                new (
-                     "_File",
-                     new MenuItem []
-                     {
-                         new (
-                              "_Quit",
-                              $"{Application.QuitKey}",
-                              () => Application.RequestStop ()
-                             )
-                     }
-                    ),
-                new (
-                     "_Options",
-                     new [] { CreateMenuShowWidth () }
-                    )
-            ]
-        };
-        top.Add (menu);
-#endif // OTHER_CONTROLS
-
-        _charMap.SelectedCodePoint = 0;
-        _charMap.SetFocus ();
-
-        Application.Run (top);
-        top.Dispose ();
-        Application.Shutdown ();
-
-        return;
-
-        void JumpEditOnAccept (object sender, CommandEventArgs e)
-        {
-            if (jumpEdit.Text.Length == 0)
-            {
-                return;
-            }
-
-            uint result = 0;
-
-            if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
-            {
-                try
-                {
-                    result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber);
-                }
-                catch (FormatException)
-                {
-                    _errorLabel.Text = "Invalid hex value";
-
-                    return;
-                }
-            }
-            else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
-            {
-                try
-                {
-                    result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber);
-                }
-                catch (FormatException)
-                {
-                    _errorLabel.Text = "Invalid hex value";
-
-                    return;
-                }
-            }
-            else
-            {
-                try
-                {
-                    result = uint.Parse (jumpEdit.Text, NumberStyles.Integer);
-                }
-                catch (FormatException)
-                {
-                    _errorLabel.Text = "Invalid value";
-
-                    return;
-                }
-            }
-
-            if (result > RuneExtensions.MaxUnicodeCodePoint)
-            {
-                _errorLabel.Text = "Beyond maximum codepoint";
-
-                return;
-            }
-
-            _errorLabel.Text = $"U+{result:x5}";
-
-            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
-
-            _categoryList.SelectedRow = table.Data
-                                             .Select ((item, index) => new { item, index })
-                                             .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)
-                                             ?.index
-                                        ?? -1;
-            _categoryList.EnsureSelectedCellIsVisible ();
-
-            // Ensure the typed glyph is selected 
-            _charMap.SelectedCodePoint = (int)result;
-
-            // Cancel the event to prevent ENTER from being handled elsewhere
-            e.Cancel = true;
-        }
-    }
-
-    private EnumerableTableSource<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
-    {
-        Func<UnicodeRange, object> orderBy;
-        var categorySort = string.Empty;
-        var startSort = string.Empty;
-        var endSort = string.Empty;
-
-        string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString ();
-
-        switch (sortByColumn)
-        {
-            case 0:
-                orderBy = r => r.Category;
-                categorySort = sortIndicator;
-
-                break;
-            case 1:
-                orderBy = r => r.Start;
-                startSort = sortIndicator;
-
-                break;
-            case 2:
-                orderBy = r => r.End;
-                endSort = sortIndicator;
-
-                break;
-            default:
-                throw new ArgumentException ("Invalid column number.");
-        }
-
-        IOrderedEnumerable<UnicodeRange> sortedRanges = descending
-                                                            ? UnicodeRange.Ranges.OrderByDescending (orderBy)
-                                                            : UnicodeRange.Ranges.OrderBy (orderBy);
-
-        return new (
-                    sortedRanges,
-                    new ()
-                    {
-                        { $"Category{categorySort}", s => s.Category },
-                        { $"Start{startSort}", s => $"{s.Start:x5}" },
-                        { $"End{endSort}", s => $"{s.End:x5}" }
-                    }
-                   );
-    }
-
-    private MenuItem CreateMenuShowWidth ()
-    {
-        var item = new MenuItem { Title = "_Show Glyph Width" };
-        item.CheckType |= MenuItemCheckStyle.Checked;
-        item.Checked = _charMap?.ShowGlyphWidths;
-        item.Action += () => { _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked); };
-
-        return item;
-    }
-
-    public override List<Key> GetDemoKeyStrokes ()
-    {
-        List<Key> keys = new ();
-
-        for (var i = 0; i < 200; i++)
-        {
-            keys.Add (Key.CursorDown);
-        }
-
-        // Category table
-        keys.Add (Key.Tab.WithShift);
-
-        // Block elements
-        keys.Add (Key.B);
-        keys.Add (Key.L);
-
-        keys.Add (Key.Tab);
-
-        for (var i = 0; i < 200; i++)
-        {
-            keys.Add (Key.CursorLeft);
-        }
-
-        return keys;
-    }
-}
-
-internal class CharMap : View, IDesignable
-{
-    private const int COLUMN_WIDTH = 3;
+    private const int COLUMN_WIDTH = 3; // Width of each column of glyphs
+    private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested
 
 
     private ContextMenu _contextMenu = new ();
     private ContextMenu _contextMenu = new ();
-    private int _rowHeight = 1;
-    private int _selected;
-    private int _start;
-
     private readonly ScrollBar _vScrollBar;
     private readonly ScrollBar _vScrollBar;
     private readonly ScrollBar _hScrollBar;
     private readonly ScrollBar _hScrollBar;
 
 
+    /// <summary>
+    ///     Initalizes a new instance.
+    /// </summary>
     public CharMap ()
     public CharMap ()
     {
     {
-        ColorScheme = Colors.ColorSchemes ["Dialog"];
+        base.ColorScheme = Colors.ColorSchemes ["Dialog"];
         CanFocus = true;
         CanFocus = true;
         CursorVisibility = CursorVisibility.Default;
         CursorVisibility = CursorVisibility.Default;
 
 
-        //ViewportSettings = ViewportSettings.AllowLocationGreaterThanContentSize;
-
-        SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, _maxCodePoint / 16 * _rowHeight + 1)); // +1 for Header
-
         AddCommand (
         AddCommand (
                     Command.Up,
                     Command.Up,
                     () =>
                     () =>
@@ -424,7 +109,7 @@ internal class CharMap : View, IDesignable
                     Command.End,
                     Command.End,
                     () =>
                     () =>
                     {
                     {
-                        SelectedCodePoint = _maxCodePoint;
+                        SelectedCodePoint = MAX_CODE_POINT;
 
 
                         return true;
                         return true;
                     }
                     }
@@ -453,7 +138,9 @@ internal class CharMap : View, IDesignable
         MouseEvent += Handle_MouseEvent;
         MouseEvent += Handle_MouseEvent;
 
 
         // Add scrollbars
         // Add scrollbars
-        Padding.Thickness = new (0, 0, 1, 0);
+        Padding!.Thickness = new (0, 0, 1, 0);
+
+        SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + 1)); // +1 for Header
 
 
         _hScrollBar = new ()
         _hScrollBar = new ()
         {
         {
@@ -463,7 +150,7 @@ internal class CharMap : View, IDesignable
             Orientation = Orientation.Horizontal,
             Orientation = Orientation.Horizontal,
             Width = Dim.Fill (1),
             Width = Dim.Fill (1),
             Size = GetContentSize ().Width - RowLabelWidth,
             Size = GetContentSize ().Width - RowLabelWidth,
-            Increment = COLUMN_WIDTH
+            Increment = COLUMN_WIDTH,
         };
         };
 
 
         _vScrollBar = new ()
         _vScrollBar = new ()
@@ -510,48 +197,45 @@ internal class CharMap : View, IDesignable
                                 Padding.Thickness = Padding.Thickness with { Bottom = 0 };
                                 Padding.Thickness = Padding.Thickness with { Bottom = 0 };
                             }
                             }
 
 
-                            _hScrollBar.ContentPosition = Viewport.X;
-                            _vScrollBar.ContentPosition = Viewport.Y;
+                            //_hScrollBar.ContentPosition = Viewport.X;
+                            //_vScrollBar.ContentPosition = Viewport.Y;
                         };
                         };
+
+        SubviewsLaidOut += (sender, args) =>
+                         {
+                             //_vScrollBar.ContentPosition = Viewport.Y;
+                             //_hScrollBar.ContentPosition = Viewport.X;
+                         };
     }
     }
 
 
-    private void Handle_MouseEvent (object sender, MouseEventArgs e)
+    private void ScrollToMakeCursorVisible (Point newCursor)
     {
     {
-        if (e.Flags == MouseFlags.WheeledDown)
+        // Adjust vertical scrolling
+        if (newCursor.Y < 1) // Header is at Y = 0
         {
         {
-            ScrollVertical (1);
-            _vScrollBar.ContentPosition = Viewport.Y;
-            e.Handled = true;
-
-            return;
+            ScrollVertical (newCursor.Y - 1);
         }
         }
-
-        if (e.Flags == MouseFlags.WheeledUp)
+        else if (newCursor.Y >= Viewport.Height)
         {
         {
-            ScrollVertical (-1);
-            _vScrollBar.ContentPosition = Viewport.Y;
-            e.Handled = true;
-
-            return;
+            ScrollVertical (newCursor.Y - Viewport.Height + 1);
         }
         }
 
 
-        if (e.Flags == MouseFlags.WheeledRight)
+        // Adjust horizontal scrolling
+        if (newCursor.X < RowLabelWidth + 1)
         {
         {
-            ScrollHorizontal (1);
-            _hScrollBar.ContentPosition = Viewport.X;
-            e.Handled = true;
-
-            return;
+            ScrollHorizontal (newCursor.X - (RowLabelWidth + 1));
         }
         }
-
-        if (e.Flags == MouseFlags.WheeledLeft)
+        else if (newCursor.X >= Viewport.Width)
         {
         {
-            ScrollHorizontal (-1);
-            _hScrollBar.ContentPosition = Viewport.X;
-            e.Handled = true;
+            ScrollHorizontal (newCursor.X - Viewport.Width + 1);
         }
         }
+
+        _vScrollBar.ContentPosition = Viewport.Y;
+        _hScrollBar.ContentPosition = Viewport.X;
     }
     }
 
 
+    #region Cursor
+
     /// <summary>Gets or sets the coordinates of the Cursor based on the SelectedCodePoint in Viewport-relative coordinates</summary>
     /// <summary>Gets or sets the coordinates of the Cursor based on the SelectedCodePoint in Viewport-relative coordinates</summary>
     public Point Cursor
     public Point Cursor
     {
     {
@@ -565,7 +249,30 @@ internal class CharMap : View, IDesignable
         set => throw new NotImplementedException ();
         set => throw new NotImplementedException ();
     }
     }
 
 
-    public static int _maxCodePoint = UnicodeRange.Ranges.Max (r => r.End);
+    public override Point? PositionCursor ()
+    {
+        if (HasFocus
+            && Cursor.X >= RowLabelWidth
+            && Cursor.X < Viewport.Width
+            && Cursor.Y > 0
+            && Cursor.Y < Viewport.Height)
+        {
+            Move (Cursor.X, Cursor.Y);
+        }
+        else
+        {
+            return null;
+        }
+
+        return Cursor;
+    }
+
+    #endregion Cursor
+
+    // ReSharper disable once InconsistentNaming
+    private static readonly int MAX_CODE_POINT = UnicodeRange.Ranges.Max (r => r.End);
+    private int _selectedCodepoint; // Currently selected codepoint
+    private int _startCodepoint; // The codepoint that will be displayed at the top of the Viewport
 
 
     /// <summary>
     /// <summary>
     ///     Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point
     ///     Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point
@@ -573,16 +280,15 @@ internal class CharMap : View, IDesignable
     /// </summary>
     /// </summary>
     public int SelectedCodePoint
     public int SelectedCodePoint
     {
     {
-        get => _selected;
+        get => _selectedCodepoint;
         set
         set
         {
         {
-            if (_selected == value)
+            if (_selectedCodepoint == value)
             {
             {
                 return;
                 return;
             }
             }
 
 
-            Point prevCursor = Cursor;
-            int newSelectedCodePoint = Math.Clamp (value, 0, _maxCodePoint);
+            int newSelectedCodePoint = Math.Clamp (value, 0, MAX_CODE_POINT);
 
 
             Point newCursor = new ()
             Point newCursor = new ()
             {
             {
@@ -590,71 +296,54 @@ internal class CharMap : View, IDesignable
                 Y = newSelectedCodePoint / 16 * _rowHeight + 1 - Viewport.Y
                 Y = newSelectedCodePoint / 16 * _rowHeight + 1 - Viewport.Y
             };
             };
 
 
+            _selectedCodepoint = newSelectedCodePoint;
+
             // Ensure the new cursor position is visible
             // Ensure the new cursor position is visible
-            EnsureCursorIsVisible (newCursor);
+            ScrollToMakeCursorVisible (newCursor);
 
 
-            _selected = newSelectedCodePoint;
             SetNeedsDraw ();
             SetNeedsDraw ();
-            SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null));
+            SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint));
         }
         }
     }
     }
 
 
-    private void EnsureCursorIsVisible (Point newCursor)
-    {
-        // Adjust vertical scrolling
-        if (newCursor.Y < 1) // Header is at Y = 0
-        {
-            ScrollVertical (newCursor.Y - 1);
-        }
-        else if (newCursor.Y >= Viewport.Height)
-        {
-            ScrollVertical (newCursor.Y - Viewport.Height + 1);
-        }
-
-        _vScrollBar.ContentPosition = Viewport.Y;
-
-        // Adjust horizontal scrolling
-        if (newCursor.X < RowLabelWidth + 1)
-        {
-            ScrollHorizontal (newCursor.X - (RowLabelWidth + 1));
-        }
-        else if (newCursor.X >= Viewport.Width)
-        {
-            ScrollHorizontal (newCursor.X - Viewport.Width + 1);
-        }
-
-        _hScrollBar.ContentPosition = Viewport.X;
-    }
+    /// <summary>
+    ///     Raised when the selected code point changes.
+    /// </summary>
+    public event EventHandler<EventArgs<int>>? SelectedCodePointChanged;
 
 
-    public bool ShowGlyphWidths
+    /// <summary>
+    ///     Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
+    ///     characters.
+    /// </summary>
+    public int StartCodePoint
     {
     {
-        get => _rowHeight == 2;
+        get => _startCodepoint;
         set
         set
         {
         {
-            _rowHeight = value ? 2 : 1;
-            SetNeedsDraw ();
+            _startCodepoint = value;
+            SelectedCodePoint = value;
         }
         }
     }
     }
 
 
     /// <summary>
     /// <summary>
-    ///     Specifies the starting offset for the character map. The default is 0x2500 which is the Box Drawing
-    ///     characters.
+    ///     Gets or sets whether the number of columns each glyph is displayed.
     /// </summary>
     /// </summary>
-    public int StartCodePoint
+    public bool ShowGlyphWidths
     {
     {
-        get => _start;
+        get => _rowHeight == 2;
         set
         set
         {
         {
-            _start = value;
-            SelectedCodePoint = value;
-            Viewport = Viewport with { Y = SelectedCodePoint / 16 * _rowHeight };
+            _rowHeight = value ? 2 : 1;
             SetNeedsDraw ();
             SetNeedsDraw ();
         }
         }
     }
     }
 
 
-    private static int RowLabelWidth => $"U+{_maxCodePoint:x5}".Length + 1;
-    private static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16;
-    public event EventHandler<ListViewItemEventArgs> Hover;
+    private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
+    private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
+
+    #region Drawing
+
+    private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1;
 
 
     protected override bool OnDrawingContent ()
     protected override bool OnDrawingContent ()
     {
     {
@@ -682,7 +371,7 @@ internal class CharMap : View, IDesignable
                 Move (x, 0);
                 Move (x, 0);
                 SetAttribute (GetHotNormalColor ());
                 SetAttribute (GetHotNormalColor ());
                 AddStr (" ");
                 AddStr (" ");
-                SetAttribute (HasFocus && cursorCol + firstColumnX == x ? ColorScheme.HotFocus : GetHotNormalColor ());
+                SetAttribute (HasFocus && cursorCol + firstColumnX == x ? GetHotFocusColor () : GetHotNormalColor ());
                 AddStr ($"{hexDigit:x}");
                 AddStr ($"{hexDigit:x}");
                 SetAttribute (GetHotNormalColor ());
                 SetAttribute (GetHotNormalColor ());
                 AddStr (" ");
                 AddStr (" ");
@@ -697,7 +386,7 @@ internal class CharMap : View, IDesignable
 
 
             int val = row * 16;
             int val = row * 16;
 
 
-            if (val > _maxCodePoint)
+            if (val > MAX_CODE_POINT)
             {
             {
                 break;
                 break;
             }
             }
@@ -770,7 +459,7 @@ internal class CharMap : View, IDesignable
                 else
                 else
                 {
                 {
                     // Draw the width of the rune
                     // Draw the width of the rune
-                    SetAttribute (ColorScheme.HotNormal);
+                    SetAttribute (GetHotNormalColor ());
                     AddStr ($"{width}");
                     AddStr ($"{width}");
                 }
                 }
 
 
@@ -784,7 +473,7 @@ internal class CharMap : View, IDesignable
             // Draw row label (U+XXXX_)
             // Draw row label (U+XXXX_)
             Move (0, y);
             Move (0, y);
 
 
-            SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? ColorScheme.HotFocus : ColorScheme.HotNormal);
+            SetAttribute (HasFocus && y + Viewport.Y - 1 == cursorRow ? GetHotFocusColor () : GetHotNormalColor ());
 
 
             if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
             if (!ShowGlyphWidths || (y + Viewport.Y) % _rowHeight > 0)
             {
             {
@@ -799,26 +488,11 @@ internal class CharMap : View, IDesignable
         return true;
         return true;
     }
     }
 
 
-    public override Point? PositionCursor ()
-    {
-        if (HasFocus
-            && Cursor.X >= RowLabelWidth
-            && Cursor.X < Viewport.Width
-            && Cursor.Y > 0
-            && Cursor.Y < Viewport.Height)
-        {
-            Move (Cursor.X, Cursor.Y);
-        }
-        else
-        {
-            return null;
-        }
-
-        return Cursor;
-    }
-
-    public event EventHandler<ListViewItemEventArgs> SelectedCodePointChanged;
-
+    /// <summary>
+    ///     Helper to convert a string into camel case.
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
     public static string ToCamelCase (string str)
     public static string ToCamelCase (string str)
     {
     {
         if (string.IsNullOrEmpty (str))
         if (string.IsNullOrEmpty (str))
@@ -834,10 +508,51 @@ internal class CharMap : View, IDesignable
         return str;
         return str;
     }
     }
 
 
-    private void CopyCodePoint () { Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; }
-    private void CopyGlyph () { Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; }
+    #endregion Drawing
+
+    #region Mouse Handling
 
 
-    private void Handle_MouseClick (object sender, MouseEventArgs me)
+    // TODO: Use this to demonstrate using a popover to show glyph info on hover
+    public event EventHandler<ListViewItemEventArgs>? Hover;
+
+    private void Handle_MouseEvent (object? sender, MouseEventArgs e)
+    {
+        if (e.Flags == MouseFlags.WheeledDown)
+        {
+            ScrollVertical (1);
+            _vScrollBar.ContentPosition = Viewport.Y;
+            e.Handled = true;
+
+            return;
+        }
+
+        if (e.Flags == MouseFlags.WheeledUp)
+        {
+            ScrollVertical (-1);
+            _vScrollBar.ContentPosition = Viewport.Y;
+            e.Handled = true;
+
+            return;
+        }
+
+        if (e.Flags == MouseFlags.WheeledRight)
+        {
+            ScrollHorizontal (1);
+            _hScrollBar.ContentPosition = Viewport.X;
+            e.Handled = true;
+
+            return;
+        }
+
+        if (e.Flags == MouseFlags.WheeledLeft)
+        {
+            ScrollHorizontal (-1);
+            _hScrollBar.ContentPosition = Viewport.X;
+            e.Handled = true;
+        }
+    }
+
+    private void Handle_MouseClick (object? sender, MouseEventArgs me)
     {
     {
         if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
         if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked)
         {
         {
@@ -864,7 +579,7 @@ internal class CharMap : View, IDesignable
 
 
         int val = row * 16 + col;
         int val = row * 16 + col;
 
 
-        if (val > _maxCodePoint)
+        if (val > MAX_CODE_POINT)
         {
         {
             return;
             return;
         }
         }
@@ -931,32 +646,41 @@ internal class CharMap : View, IDesignable
         }
         }
     }
     }
 
 
+    #endregion Mouse Handling
+
+    #region Details Dialog
+
     private void ShowDetails ()
     private void ShowDetails ()
     {
     {
-        var client = new UcdApiClient ();
+        UcdApiClient? client = new ();
         var decResponse = string.Empty;
         var decResponse = string.Empty;
         var getCodePointError = string.Empty;
         var getCodePointError = string.Empty;
 
 
-        var waitIndicator = new Dialog
+        Dialog? waitIndicator = new ()
         {
         {
             Title = "Getting Code Point Information",
             Title = "Getting Code Point Information",
             X = Pos.Center (),
             X = Pos.Center (),
             Y = Pos.Center (),
             Y = Pos.Center (),
-            Height = 7,
-            Width = 50,
-            Buttons = [new () { Text = "Cancel" }]
+            Width = 40,
+            Height = 10,
+            Buttons = [new () { Text = "_Cancel" }]
         };
         };
 
 
         var errorLabel = new Label
         var errorLabel = new Label
         {
         {
             Text = UcdApiClient.BaseUrl,
             Text = UcdApiClient.BaseUrl,
             X = 0,
             X = 0,
-            Y = 1,
+            Y = 0,
             Width = Dim.Fill (),
             Width = Dim.Fill (),
-            Height = Dim.Fill (1),
+            Height = Dim.Fill (3),
             TextAlignment = Alignment.Center
             TextAlignment = Alignment.Center
         };
         };
-        var spinner = new SpinnerView { X = Pos.Center (), Y = Pos.Center (), Style = new Aesthetic () };
+        var spinner = new SpinnerView
+        {
+            X = Pos.Center (),
+            Y = Pos.Bottom (errorLabel),
+            Style = new SpinnerStyle.Aesthetic ()
+        };
         spinner.AutoSpin = true;
         spinner.AutoSpin = true;
         waitIndicator.Add (errorLabel);
         waitIndicator.Add (errorLabel);
         waitIndicator.Add (spinner);
         waitIndicator.Add (spinner);
@@ -1000,30 +724,30 @@ internal class CharMap : View, IDesignable
                                                         document.RootElement,
                                                         document.RootElement,
                                                         new
                                                         new
                                                             JsonSerializerOptions
                                                             JsonSerializerOptions
-                                                            { WriteIndented = true }
+                                                        { WriteIndented = true }
                                                        );
                                                        );
             }
             }
 
 
-            var title = $"{ToCamelCase (name)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
+            var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}";
 
 
-            var copyGlyph = new Button { Text = "Copy _Glyph" };
-            var copyCP = new Button { Text = "Copy Code _Point" };
-            var cancel = new Button { Text = "Cancel" };
+            Button? copyGlyph = new () { Text = "Copy _Glyph" };
+            Button? copyCodepoint = new () { Text = "Copy Code _Point" };
+            Button? cancel = new () { Text = "Cancel" };
 
 
-            var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCP, cancel] };
+            var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] };
 
 
             copyGlyph.Accepting += (s, a) =>
             copyGlyph.Accepting += (s, a) =>
                                    {
                                    {
                                        CopyGlyph ();
                                        CopyGlyph ();
-                                       dlg.RequestStop ();
+                                       dlg!.RequestStop ();
                                    };
                                    };
 
 
-            copyCP.Accepting += (s, a) =>
-                                {
-                                    CopyCodePoint ();
-                                    dlg.RequestStop ();
-                                };
-            cancel.Accepting += (s, a) => dlg.RequestStop ();
+            copyCodepoint.Accepting += (s, a) =>
+                                       {
+                                           CopyCodePoint ();
+                                           dlg!.RequestStop ();
+                                       };
+            cancel.Accepting += (s, a) => dlg!.RequestStop ();
 
 
             var rune = (Rune)SelectedCodePoint;
             var rune = (Rune)SelectedCodePoint;
             var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
             var label = new Label { Text = "IsAscii: ", X = 0, Y = 0 };
@@ -1097,129 +821,13 @@ internal class CharMap : View, IDesignable
             MessageBox.ErrorQuery (
             MessageBox.ErrorQuery (
                                    "Code Point API",
                                    "Code Point API",
                                    $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
                                    $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.",
-                                   "Ok"
+                                   "_Ok"
                                   );
                                   );
         }
         }
 
 
         // BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug
         // BUGBUG: This is a workaround for some weird ScrollView related mouse grab bug
         Application.GrabMouse (this);
         Application.GrabMouse (this);
     }
     }
-}
-
-public class UcdApiClient
-{
-    public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
-    private static readonly HttpClient _httpClient = new ();
-
-    public async Task<string> GetChars (string chars)
-    {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
-
-    public async Task<string> GetCharsName (string chars)
-    {
-        HttpResponseMessage response =
-            await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
 
 
-    public async Task<string> GetCodepointDec (int dec)
-    {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
-
-    public async Task<string> GetCodepointHex (string hex)
-    {
-        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
-        response.EnsureSuccessStatusCode ();
-
-        return await response.Content.ReadAsStringAsync ();
-    }
-}
-
-internal class UnicodeRange
-{
-    public static List<UnicodeRange> Ranges = GetRanges ();
-
-    public string Category;
-    public int End;
-    public int Start;
-
-    public UnicodeRange (int start, int end, string category)
-    {
-        Start = start;
-        End = end;
-        Category = category;
-    }
-
-    public static List<UnicodeRange> GetRanges ()
-    {
-        IEnumerable<UnicodeRange> ranges =
-            from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public)
-            let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange
-            let name = string.IsNullOrEmpty (r.Name)
-                           ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}"
-                           : r.Name
-            where name != "None" && name != "All"
-            select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name);
-
-        // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0
-        List<UnicodeRange> nonBmpRanges = new ()
-        {
-            new (
-                 0x1F130,
-                 0x1F149,
-                 "Squared Latin Capital Letters"
-                ),
-            new (
-                 0x12400,
-                 0x1240f,
-                 "Cuneiform Numbers and Punctuation"
-                ),
-            new (0x10000, 0x1007F, "Linear B Syllabary"),
-            new (0x10080, 0x100FF, "Linear B Ideograms"),
-            new (0x10100, 0x1013F, "Aegean Numbers"),
-            new (0x10300, 0x1032F, "Old Italic"),
-            new (0x10330, 0x1034F, "Gothic"),
-            new (0x10380, 0x1039F, "Ugaritic"),
-            new (0x10400, 0x1044F, "Deseret"),
-            new (0x10450, 0x1047F, "Shavian"),
-            new (0x10480, 0x104AF, "Osmanya"),
-            new (0x10800, 0x1083F, "Cypriot Syllabary"),
-            new (
-                 0x1D000,
-                 0x1D0FF,
-                 "Byzantine Musical Symbols"
-                ),
-            new (0x1D100, 0x1D1FF, "Musical Symbols"),
-            new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"),
-            new (
-                 0x1D400,
-                 0x1D7FF,
-                 "Mathematical Alphanumeric Symbols"
-                ),
-            new (0x1F600, 0x1F532, "Emojis Symbols"),
-            new (
-                 0x20000,
-                 0x2A6DF,
-                 "CJK Unified Ideographs Extension B"
-                ),
-            new (
-                 0x2F800,
-                 0x2FA1F,
-                 "CJK Compatibility Ideographs Supplement"
-                ),
-            new (0xE0000, 0xE007F, "Tags")
-        };
-
-        return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList ();
-    }
+    #endregion Details Dialog
 }
 }

+ 342 - 0
UICatalog/Scenarios/CharacterMap/CharacterMap.cs

@@ -0,0 +1,342 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     This Scenario demonstrates building a custom control (a class deriving from View) that: - Provides a
+///     "Character Map" application (like Windows' charmap.exe). - Helps test unicode character rendering in Terminal.Gui -
+///     Illustrates how to do infinite scrolling
+/// </summary>
+/// <remarks>
+///     See <see href="CharacterMap/README.md"/>.
+/// </remarks>
+[ScenarioMetadata ("Character Map", "Unicode viewer. Demos infinite content drawing and scrolling.")]
+[ScenarioCategory ("Text and Formatting")]
+[ScenarioCategory ("Drawing")]
+[ScenarioCategory ("Controls")]
+[ScenarioCategory ("Layout")]
+[ScenarioCategory ("Scrolling")]
+public class CharacterMap : Scenario
+{
+    private Label? _errorLabel;
+    private TableView? _categoryList;
+    private CharMap? _charMap;
+
+    // Don't create a Window, just return the top-level view
+    public override void Main ()
+    {
+        Application.Init ();
+
+        var top = new Window
+        {
+            BorderStyle = LineStyle.None
+        };
+
+        _charMap = new ()
+        {
+            X = 0,
+            Y = 1,
+            Width = Dim.Fill (Dim.Func (() => _categoryList!.Frame.Width)),
+            Height = Dim.Fill ()
+        };
+        top.Add (_charMap);
+
+        var jumpLabel = new Label
+        {
+            X = Pos.Right (_charMap) + 1,
+            Y = Pos.Y (_charMap),
+            HotKeySpecifier = (Rune)'_',
+            Text = "_Jump To:"
+        };
+        top.Add (jumpLabel);
+
+        var jumpEdit = new TextField
+        {
+            X = Pos.Right (jumpLabel) + 1,
+            Y = Pos.Y (_charMap),
+            Width = 17,
+            Caption = "e.g. 01BE3 or ✈",
+        };
+        top.Add (jumpEdit);
+
+        _charMap.SelectedCodePointChanged += (sender, args) => jumpEdit.Text = ((Rune)args.CurrentValue).ToString ();
+
+        _errorLabel = new ()
+        {
+            X = Pos.Right (jumpEdit) + 1,
+            Y = Pos.Y (_charMap),
+            ColorScheme = Colors.ColorSchemes ["error"],
+            Text = "err",
+            Visible = false
+
+        };
+        top.Add (_errorLabel);
+
+        jumpEdit.Accepting += JumpEditOnAccept;
+
+        _categoryList = new () { 
+            X = Pos.Right (_charMap), 
+            Y = Pos.Bottom (jumpLabel), 
+            Height = Dim.Fill (),
+        };
+        _categoryList.FullRowSelect = true;
+        _categoryList.MultiSelect = false;
+
+        _categoryList.Style.ShowVerticalCellLines = false;
+        _categoryList.Style.AlwaysShowHeaders = true;
+
+        var isDescending = false;
+
+        _categoryList.Table = CreateCategoryTable (0, isDescending);
+
+        // if user clicks the mouse in TableView
+        _categoryList.MouseClick += (s, e) =>
+                                    {
+                                        _categoryList.ScreenToCell (e.Position, out int? clickedCol);
+
+                                        if (clickedCol != null && e.Flags.HasFlag (MouseFlags.Button1Clicked))
+                                        {
+                                            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
+                                            string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category;
+                                            isDescending = !isDescending;
+
+                                            _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending);
+
+                                            table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
+
+                                            _categoryList.SelectedRow = table.Data
+                                                                             .Select ((item, index) => new { item, index })
+                                                                             .FirstOrDefault (x => x.item.Category == prevSelection)
+                                                                             ?.index
+                                                                        ?? -1;
+                                        }
+                                    };
+
+        int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ());
+
+        _categoryList.Style.ColumnStyles.Add (
+                                              0,
+                                              new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }
+                                             );
+        _categoryList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1, MinWidth = 6 });
+        _categoryList.Style.ColumnStyles.Add (2, new () { MaxWidth = 1, MinWidth = 6 });
+
+        _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4;
+
+        _categoryList.SelectedCellChanged += (s, args) =>
+                                             {
+                                                 EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList.Table;
+                                                 _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start;
+                                                 jumpEdit.Text = $"U+{_charMap.SelectedCodePoint:x5}";
+                                             };
+
+        top.Add (_categoryList);
+
+        var menu = new MenuBar
+        {
+            Menus =
+            [
+                new (
+                     "_File",
+                     new MenuItem []
+                     {
+                         new (
+                              "_Quit",
+                              $"{Application.QuitKey}",
+                              () => Application.RequestStop ()
+                             )
+                     }
+                    ),
+                new (
+                     "_Options",
+                     new [] { CreateMenuShowWidth () }
+                    )
+            ]
+        };
+        top.Add (menu);
+
+        _charMap.SelectedCodePoint = 0;
+        _charMap.SetFocus ();
+
+        Application.Run (top);
+        top.Dispose ();
+        Application.Shutdown ();
+
+        return;
+
+        void JumpEditOnAccept (object? sender, CommandEventArgs e)
+        {
+            if (jumpEdit.Text.Length == 0)
+            {
+                return;
+            }
+
+            _errorLabel.Visible = true;
+
+            uint result = 0;
+
+            if (jumpEdit.Text.Length == 1)
+            {
+                result = (uint)jumpEdit.Text.ToRunes () [0].Value;
+            }
+            else if (jumpEdit.Text.StartsWith ("U+", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
+            {
+                try
+                {
+                    result = uint.Parse (jumpEdit.Text [2..], NumberStyles.HexNumber);
+                }
+                catch (FormatException)
+                {
+                    _errorLabel.Text = "Invalid hex value";
+
+                    return;
+                }
+            }
+            else if (jumpEdit.Text.StartsWith ("0", StringComparison.OrdinalIgnoreCase) || jumpEdit.Text.StartsWith ("\\u"))
+            {
+                try
+                {
+                    result = uint.Parse (jumpEdit.Text, NumberStyles.HexNumber);
+                }
+                catch (FormatException)
+                {
+                    _errorLabel.Text = "Invalid hex value";
+
+                    return;
+                }
+            }
+            else
+            {
+                try
+                {
+                    result = uint.Parse (jumpEdit.Text, NumberStyles.Integer);
+                }
+                catch (FormatException)
+                {
+                    _errorLabel.Text = "Invalid value";
+
+                    return;
+                }
+            }
+
+            if (result > RuneExtensions.MaxUnicodeCodePoint)
+            {
+                _errorLabel.Text = "Beyond maximum codepoint";
+
+                return;
+            }
+
+            _errorLabel.Visible = false;
+
+            EnumerableTableSource<UnicodeRange> table = (EnumerableTableSource<UnicodeRange>)_categoryList!.Table;
+
+            _categoryList.SelectedRow = table.Data
+                                             .Select ((item, index) => new { item, index })
+                                             .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)
+                                             ?.index
+                                        ?? -1;
+            _categoryList.EnsureSelectedCellIsVisible ();
+
+            // Ensure the typed glyph is selected 
+            _charMap.SelectedCodePoint = (int)result;
+            _charMap.SetFocus ();
+
+            // Cancel the event to prevent ENTER from being handled elsewhere
+            e.Cancel = true;
+        }
+    }
+
+    private EnumerableTableSource<UnicodeRange> CreateCategoryTable (int sortByColumn, bool descending)
+    {
+        Func<UnicodeRange, object> orderBy;
+        var categorySort = string.Empty;
+        var startSort = string.Empty;
+        var endSort = string.Empty;
+
+        string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString ();
+
+        switch (sortByColumn)
+        {
+            case 0:
+                orderBy = r => r.Category;
+                categorySort = sortIndicator;
+
+                break;
+            case 1:
+                orderBy = r => r.Start;
+                startSort = sortIndicator;
+
+                break;
+            case 2:
+                orderBy = r => r.End;
+                endSort = sortIndicator;
+
+                break;
+            default:
+                throw new ArgumentException ("Invalid column number.");
+        }
+
+        IOrderedEnumerable<UnicodeRange> sortedRanges = descending
+                                                            ? UnicodeRange.Ranges.OrderByDescending (orderBy)
+                                                            : UnicodeRange.Ranges.OrderBy (orderBy);
+
+        return new (
+                    sortedRanges,
+                    new ()
+                    {
+                        { $"Category{categorySort}", s => s.Category },
+                        { $"Start{startSort}", s => $"{s.Start:x5}" },
+                        { $"End{endSort}", s => $"{s.End:x5}" }
+                    }
+                   );
+    }
+
+    private MenuItem CreateMenuShowWidth ()
+    {
+        var item = new MenuItem { Title = "_Show Glyph Width" };
+        item.CheckType |= MenuItemCheckStyle.Checked;
+        item.Checked = _charMap?.ShowGlyphWidths;
+        item.Action += () =>
+                       {
+                           if (_charMap is { })
+                           {
+                               _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked)!;
+                           }
+                       };
+
+        return item;
+    }
+
+    public override List<Key> GetDemoKeyStrokes ()
+    {
+        List<Key> keys = new ();
+
+        for (var i = 0; i < 200; i++)
+        {
+            keys.Add (Key.CursorDown);
+        }
+
+        // Category table
+        keys.Add (Key.Tab.WithShift);
+
+        // Block elements
+        keys.Add (Key.B);
+        keys.Add (Key.L);
+
+        keys.Add (Key.Tab);
+
+        for (var i = 0; i < 200; i++)
+        {
+            keys.Add (Key.CursorLeft);
+        }
+
+        return keys;
+    }
+}

+ 11 - 0
UICatalog/Scenarios/CharacterMap/README.md

@@ -0,0 +1,11 @@
+# CharacterMap Scenario Deep Dive
+
+The CharacterMap Scenario is an example of the following Terminal.Gui concepts:
+
+- **Complex and High-Performnt Drawing** - 
+- **Virtual Content Scrolling** -
+- **Advanced ScrollBar Control** -
+- **Unicode wide-glyph Rendering** -
+- **Advanced Layout** -
+- **Cursor Management** -
+- **Context Menu** - 

+ 48 - 0
UICatalog/Scenarios/CharacterMap/UcdApiClient.cs

@@ -0,0 +1,48 @@
+#nullable enable
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     A helper class for accessing the ucdapi.org API.
+/// </summary>
+public class UcdApiClient
+{
+    public const string BaseUrl = "https://ucdapi.org/unicode/latest/";
+    private static readonly HttpClient _httpClient = new ();
+
+    public async Task<string> GetChars (string chars)
+    {
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+
+    public async Task<string> GetCharsName (string chars)
+    {
+        HttpResponseMessage response =
+            await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+
+    public async Task<string> GetCodepointDec (int dec)
+    {
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+
+    public async Task<string> GetCodepointHex (string hex)
+    {
+        HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}");
+        response.EnsureSuccessStatusCode ();
+
+        return await response.Content.ReadAsStringAsync ();
+    }
+}

+ 101 - 0
UICatalog/Scenarios/CharacterMap/UnicodeRange.cs

@@ -0,0 +1,101 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text.Unicode;
+
+namespace UICatalog.Scenarios;
+
+/// <summary>
+///     Represents all of the Uniicode ranges.from System.Text.Unicode.UnicodeRange plus
+///     the non-BMP ranges not included.
+/// </summary>
+public class UnicodeRange (int start, int end, string category)
+{
+    /// <summary>
+    ///     Gets the list of all ranges.
+    /// </summary>
+    public static List<UnicodeRange> Ranges => GetRanges ();
+
+    /// <summary>
+    ///     The category.
+    /// </summary>
+    public string Category { get; set; } = category;
+
+    /// <summary>
+    ///     Te codepoint at the start of the range.
+    /// </summary>
+    public int Start { get; set; } = start;
+
+    /// <summary>
+    ///     The codepoint at the end of the range.
+    /// </summary>
+    public int End { get; set; } = end;
+
+    /// <summary>
+    ///     Gets the list of all ranges..
+    /// </summary>
+    /// <returns></returns>
+    public static List<UnicodeRange> GetRanges ()
+    {
+        IEnumerable<UnicodeRange> ranges =
+            from r in typeof (UnicodeRanges).GetProperties (BindingFlags.Static | BindingFlags.Public)
+            let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange
+            let name = string.IsNullOrEmpty (r.Name)
+                           ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}"
+                           : r.Name
+            where name != "None" && name != "All"
+            select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name);
+
+        // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0
+        List<UnicodeRange> nonBmpRanges = new ()
+        {
+            new (
+                 0x1F130,
+                 0x1F149,
+                 "Squared Latin Capital Letters"
+                ),
+            new (
+                 0x12400,
+                 0x1240f,
+                 "Cuneiform Numbers and Punctuation"
+                ),
+            new (0x10000, 0x1007F, "Linear B Syllabary"),
+            new (0x10080, 0x100FF, "Linear B Ideograms"),
+            new (0x10100, 0x1013F, "Aegean Numbers"),
+            new (0x10300, 0x1032F, "Old Italic"),
+            new (0x10330, 0x1034F, "Gothic"),
+            new (0x10380, 0x1039F, "Ugaritic"),
+            new (0x10400, 0x1044F, "Deseret"),
+            new (0x10450, 0x1047F, "Shavian"),
+            new (0x10480, 0x104AF, "Osmanya"),
+            new (0x10800, 0x1083F, "Cypriot Syllabary"),
+            new (
+                 0x1D000,
+                 0x1D0FF,
+                 "Byzantine Musical Symbols"
+                ),
+            new (0x1D100, 0x1D1FF, "Musical Symbols"),
+            new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"),
+            new (
+                 0x1D400,
+                 0x1D7FF,
+                 "Mathematical Alphanumeric Symbols"
+                ),
+            new (0x1F600, 0x1F532, "Emojis Symbols"),
+            new (
+                 0x20000,
+                 0x2A6DF,
+                 "CJK Unified Ideographs Extension B"
+                ),
+            new (
+                 0x2F800,
+                 0x2FA1F,
+                 "CJK Compatibility Ideographs Supplement"
+                ),
+            new (0xE0000, 0xE007F, "Tags")
+        };
+
+        return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList ();
+    }
+}

+ 11 - 43
UICatalog/Scenarios/ScrollBarDemo.cs

@@ -4,12 +4,10 @@ using Terminal.Gui;
 
 
 namespace UICatalog.Scenarios;
 namespace UICatalog.Scenarios;
 
 
-[ScenarioMetadata ("ScrollBar Demo", "Demonstrates using ScrollBar view.")]
+[ScenarioMetadata ("ScrollBar Demo", "Demonstrates ScrollBar.")]
 [ScenarioCategory ("Scrolling")]
 [ScenarioCategory ("Scrolling")]
 public class ScrollBarDemo : Scenario
 public class ScrollBarDemo : Scenario
 {
 {
-    private ViewDiagnosticFlags _diagnosticFlags;
-
     public override void Main ()
     public override void Main ()
     {
     {
         Application.Init ();
         Application.Init ();
@@ -29,23 +27,22 @@ public class ScrollBarDemo : Scenario
             X = Pos.Right (editor),
             X = Pos.Right (editor),
             Width = Dim.Fill (),
             Width = Dim.Fill (),
             Height = Dim.Fill (),
             Height = Dim.Fill (),
-            ColorScheme = Colors.ColorSchemes ["Base"]
+            ColorScheme = Colors.ColorSchemes ["Base"],
+            Arrangement = ViewArrangement.Resizable
         };
         };
+        frameView!.Padding!.Thickness = new (1);
+        frameView.Padding.Diagnostics = ViewDiagnosticFlags.Ruler;
         app.Add (frameView);
         app.Add (frameView);
 
 
         var scrollBar = new ScrollBar
         var scrollBar = new ScrollBar
         {
         {
             X = Pos.AnchorEnd (),
             X = Pos.AnchorEnd (),
-            AutoHide = false
+            AutoHide = false,
+            Size = 100,
             //ShowPercent = true
             //ShowPercent = true
         };
         };
         frameView.Add (scrollBar);
         frameView.Add (scrollBar);
 
 
-        app.Loaded += (s, e) =>
-                      {
-                          scrollBar.Size = scrollBar.Viewport.Height;
-                      };
-
         int GetMaxLabelWidth (int groupId)
         int GetMaxLabelWidth (int groupId)
         {
         {
             return frameView.Subviews.Max (
             return frameView.Subviews.Max (
@@ -134,7 +131,6 @@ public class ScrollBarDemo : Scenario
                                                      scrollBar.Y = 0;
                                                      scrollBar.Y = 0;
                                                      scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scrollBar.SuperView.GetContentSize ().Width);
                                                      scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scrollBar.SuperView.GetContentSize ().Width);
                                                      scrollBar.Width = scrollWidthHeight.Value;
                                                      scrollBar.Width = scrollWidthHeight.Value;
-                                                     scrollBar.Size /= 3;
                                                  }
                                                  }
                                                  else
                                                  else
                                                  {
                                                  {
@@ -143,7 +139,6 @@ public class ScrollBarDemo : Scenario
                                                      scrollBar.Y = Pos.AnchorEnd ();
                                                      scrollBar.Y = Pos.AnchorEnd ();
                                                      scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scrollBar.SuperView.GetContentSize ().Height);
                                                      scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scrollBar.SuperView.GetContentSize ().Height);
                                                      scrollBar.Height = scrollWidthHeight.Value;
                                                      scrollBar.Height = scrollWidthHeight.Value;
-                                                     scrollBar.Size *= 3;
                                                  }
                                                  }
                                              };
                                              };
 
 
@@ -189,34 +184,14 @@ public class ScrollBarDemo : Scenario
         };
         };
         frameView.Add (lblSliderPosition);
         frameView.Add (lblSliderPosition);
 
 
-        NumericUpDown<int> scrollSliderPosition = new ()
+        Label scrollSliderPosition = new ()
         {
         {
-            Value = scrollBar.SliderPosition,
+            Text = scrollBar.GetSliderPosition ().ToString (),
             X = Pos.Right (lblSliderPosition) + 1,
             X = Pos.Right (lblSliderPosition) + 1,
             Y = Pos.Top (lblSliderPosition)
             Y = Pos.Top (lblSliderPosition)
         };
         };
         frameView.Add (scrollSliderPosition);
         frameView.Add (scrollSliderPosition);
 
 
-        scrollSliderPosition.ValueChanging += (s, e) =>
-                                              {
-                                                  if (e.NewValue < 0)
-                                                  {
-                                                      e.Cancel = true;
-
-                                                      return;
-                                                  }
-
-                                                  if (scrollBar.SliderPosition != e.NewValue)
-                                                  {
-                                                      scrollBar.SliderPosition = e.NewValue;
-                                                  }
-
-                                                  if (scrollBar.SliderPosition != e.NewValue)
-                                                  {
-                                                      e.Cancel = true;
-                                                  }
-                                              };
-
         var lblContentPosition = new Label
         var lblContentPosition = new Label
         {
         {
             Text = "_ContentPosition:",
             Text = "_ContentPosition:",
@@ -229,7 +204,7 @@ public class ScrollBarDemo : Scenario
 
 
         NumericUpDown<int> scrollContentPosition = new ()
         NumericUpDown<int> scrollContentPosition = new ()
         {
         {
-            Value = scrollBar.SliderPosition,
+            Value = scrollBar.GetSliderPosition (),
             X = Pos.Right (lblContentPosition) + 1,
             X = Pos.Right (lblContentPosition) + 1,
             Y = Pos.Top (lblContentPosition)
             Y = Pos.Top (lblContentPosition)
         };
         };
@@ -342,22 +317,15 @@ public class ScrollBarDemo : Scenario
                                          }
                                          }
                                      };
                                      };
 
 
-            scrollBar.SliderPositionChanging += (s, e) =>
-                                          {
-                                              eventLog.Log ($"SliderPositionChanging: {e.CurrentValue}");
-                                              eventLog.Log ($"  NewValue: {e.NewValue}");
-                                          };
-
             scrollBar.SliderPositionChanged += (s, e) =>
             scrollBar.SliderPositionChanged += (s, e) =>
                                          {
                                          {
                                              eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}");
                                              eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}");
                                              eventLog.Log ($"  ContentPosition: {scrollBar.ContentPosition}");
                                              eventLog.Log ($"  ContentPosition: {scrollBar.ContentPosition}");
-                                             scrollSliderPosition.Value = e.CurrentValue;
+                                             scrollSliderPosition.Text = e.CurrentValue.ToString ();
                                          };
                                          };
 
 
             editor.Initialized += (s, e) =>
             editor.Initialized += (s, e) =>
                                   {
                                   {
-                                      scrollBar.Size = int.Max (app.GetContentSize ().Height * 2, app.GetContentSize ().Width * 2);
                                       editor.ViewToEdit = scrollBar;
                                       editor.ViewToEdit = scrollBar;
                                   };
                                   };
 
 

+ 213 - 150
UICatalog/Scenarios/ScrollDemo.cs

@@ -1,22 +1,21 @@
 using System;
 using System;
+using System.Linq;
 using Terminal.Gui;
 using Terminal.Gui;
 
 
 namespace UICatalog.Scenarios;
 namespace UICatalog.Scenarios;
 
 
-[ScenarioMetadata ("Scroll Demo", "Demonstrates using Scroll view.")]
-[ScenarioCategory ("Drawing")]
+[ScenarioMetadata ("Scroll Demo", "Demonstrates Scroll.")]
 [ScenarioCategory ("Scrolling")]
 [ScenarioCategory ("Scrolling")]
 public class ScrollDemo : Scenario
 public class ScrollDemo : Scenario
 {
 {
-    private ViewDiagnosticFlags _diagnosticFlags;
-
     public override void Main ()
     public override void Main ()
     {
     {
         Application.Init ();
         Application.Init ();
 
 
         Window app = new ()
         Window app = new ()
         {
         {
-            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"
+            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}",
+            Arrangement = ViewArrangement.Fixed
         };
         };
 
 
         var editor = new AdornmentsEditor ();
         var editor = new AdornmentsEditor ();
@@ -28,25 +27,40 @@ public class ScrollDemo : Scenario
             X = Pos.Right (editor),
             X = Pos.Right (editor),
             Width = Dim.Fill (),
             Width = Dim.Fill (),
             Height = Dim.Fill (),
             Height = Dim.Fill (),
-            ColorScheme = Colors.ColorSchemes ["Base"]
+            ColorScheme = Colors.ColorSchemes ["Base"],
+            Arrangement = ViewArrangement.Resizable
         };
         };
+        frameView.Padding.Thickness = new (1);
+        frameView.Padding.Diagnostics = ViewDiagnosticFlags.Ruler;
         app.Add (frameView);
         app.Add (frameView);
 
 
         var scroll = new Scroll
         var scroll = new Scroll
         {
         {
             X = Pos.AnchorEnd (),
             X = Pos.AnchorEnd (),
-            ShowPercent = true
+            Size = 1000,
         };
         };
         frameView.Add (scroll);
         frameView.Add (scroll);
 
 
-        app.Loaded += (s, e) =>
-                                 {
-                                     scroll.Size = frameView.Viewport.Height;
-                                 };
+        int GetMaxLabelWidth (int groupId)
+        {
+            return frameView.Subviews.Max (
+                                           v =>
+                                           {
+                                               if (v.Y.Has<PosAlign> (out var pos) && pos.GroupId == groupId)
+                                               {
+                                                   return v.Text.GetColumns ();
+                                               }
+
+                                               return 0;
+                                           });
+        }
 
 
         var lblWidthHeight = new Label
         var lblWidthHeight = new Label
         {
         {
-            Text = "Width/Height:"
+            Text = "_Width/Height:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, AlignmentModes.StartToEnd, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
         };
         };
         frameView.Add (lblWidthHeight);
         frameView.Add (lblWidthHeight);
 
 
@@ -59,71 +73,79 @@ public class ScrollDemo : Scenario
         frameView.Add (scrollWidthHeight);
         frameView.Add (scrollWidthHeight);
 
 
         scrollWidthHeight.ValueChanging += (s, e) =>
         scrollWidthHeight.ValueChanging += (s, e) =>
-                                           {
-                                               if (e.NewValue < 1
-                                                   || (e.NewValue
-                                                       > (scroll.Orientation == Orientation.Vertical
-                                                              ? scroll.SuperView?.GetContentSize ().Width
-                                                              : scroll.SuperView?.GetContentSize ().Height)))
-                                               {
-                                                   // TODO: This must be handled in the ScrollSlider if Width and Height being virtual
-                                                   e.Cancel = true;
+        {
+            if (e.NewValue < 1
+                || (e.NewValue
+                    > (scroll.Orientation == Orientation.Vertical
+                           ? scroll.SuperView?.GetContentSize ().Width
+                           : scroll.SuperView?.GetContentSize ().Height)))
+            {
+                // TODO: This must be handled in the ScrollSlider if Width and Height being virtual
+                e.Cancel = true;
+
+                return;
+            }
+
+            if (scroll.Orientation == Orientation.Vertical)
+            {
+                scroll.Width = e.NewValue;
+            }
+            else
+            {
+                scroll.Height = e.NewValue;
+            }
+        };
 
 
-                                                   return;
-                                               }
 
 
-                                               if (scroll.Orientation == Orientation.Vertical)
-                                               {
-                                                   scroll.Width = e.NewValue;
-                                               }
-                                               else
-                                               {
-                                                   scroll.Height = e.NewValue;
-                                               }
-                                           };
+        var lblOrientationabel = new Label
+        {
+            Text = "_Orientation:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        frameView.Add (lblOrientationabel);
 
 
         var rgOrientation = new RadioGroup
         var rgOrientation = new RadioGroup
         {
         {
-            Y = Pos.Bottom (lblWidthHeight),
+            X = Pos.Right (lblOrientationabel) + 1,
+            Y = Pos.Top (lblOrientationabel),
             RadioLabels = ["Vertical", "Horizontal"],
             RadioLabels = ["Vertical", "Horizontal"],
             Orientation = Orientation.Horizontal
             Orientation = Orientation.Horizontal
         };
         };
         frameView.Add (rgOrientation);
         frameView.Add (rgOrientation);
 
 
         rgOrientation.SelectedItemChanged += (s, e) =>
         rgOrientation.SelectedItemChanged += (s, e) =>
-                                             {
-                                                 if (e.SelectedItem == e.PreviousSelectedItem)
-                                                 {
-                                                     return;
-                                                 }
-
-                                                 if (rgOrientation.SelectedItem == 0)
-                                                 {
-                                                     scroll.Orientation = Orientation.Vertical;
-                                                     scroll.X = Pos.AnchorEnd ();
-                                                     scroll.Y = 0;
-                                                     scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Width);
-                                                     scroll.Width = scrollWidthHeight.Value;
-                                                     scroll.Height = Dim.Fill ();
-                                                     scroll.Size /= 3;
-                                                 }
-                                                 else
-                                                 {
-                                                     scroll.Orientation = Orientation.Horizontal;
-                                                     scroll.X = 0;
-                                                     scroll.Y = Pos.AnchorEnd ();
-                                                     scroll.Width = Dim.Fill ();
-
-                                                     scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Height);
-                                                     scroll.Height = scrollWidthHeight.Value;
-                                                     scroll.Size *= 3;
-                                                 }
-                                             };
+        {
+            if (e.SelectedItem == e.PreviousSelectedItem)
+            {
+                return;
+            }
+
+            if (rgOrientation.SelectedItem == 0)
+            {
+                scroll.Orientation = Orientation.Vertical;
+                scroll.X = Pos.AnchorEnd ();
+                scroll.Y = 0;
+                scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Width);
+                scroll.Width = scrollWidthHeight.Value;
+            }
+            else
+            {
+                scroll.Orientation = Orientation.Horizontal;
+                scroll.X = 0;
+                scroll.Y = Pos.AnchorEnd ();
+                scrollWidthHeight.Value = Math.Min (scrollWidthHeight.Value, scroll.SuperView.GetContentSize ().Height);
+                scroll.Height = scrollWidthHeight.Value;
+            }
+        };
 
 
         var lblSize = new Label
         var lblSize = new Label
         {
         {
-            Y = Pos.Bottom (rgOrientation),
-            Text = "Size:"
+            Text = "_Size:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
         };
         };
         frameView.Add (lblSize);
         frameView.Add (lblSize);
 
 
@@ -133,106 +155,109 @@ public class ScrollDemo : Scenario
             X = Pos.Right (lblSize) + 1,
             X = Pos.Right (lblSize) + 1,
             Y = Pos.Top (lblSize)
             Y = Pos.Top (lblSize)
         };
         };
-        scroll.SizeChanged += (sender, args) => scrollSize.Value = args.CurrentValue;
         frameView.Add (scrollSize);
         frameView.Add (scrollSize);
 
 
         scrollSize.ValueChanging += (s, e) =>
         scrollSize.ValueChanging += (s, e) =>
-                                    {
-                                        if (e.NewValue < 0)
-                                        {
-                                            e.Cancel = true;
+        {
+            if (e.NewValue < 0)
+            {
+                e.Cancel = true;
 
 
-                                            return;
-                                        }
+                return;
+            }
 
 
-                                        if (scroll.Size != e.NewValue)
-                                        {
-                                            scroll.Size = e.NewValue;
-                                        }
-                                    };
+            if (scroll.Size != e.NewValue)
+            {
+                scroll.Size = e.NewValue;
+            }
+        };
 
 
-        var lblPosition = new Label
+        var lblSliderPosition = new Label
         {
         {
-            Y = Pos.Bottom (lblSize),
-            Text = "Position:"
+            Text = "_SliderPosition:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+
         };
         };
-        frameView.Add (lblPosition);
+        frameView.Add (lblSliderPosition);
 
 
-        NumericUpDown<int> scrollPosition = new ()
+        Label scrollSliderPosition = new ()
         {
         {
-            Value = scroll.SliderPosition,
-            X = Pos.Right (lblPosition) + 1,
-            Y = Pos.Top (lblPosition)
+            Text = scroll.GetSliderPosition ().ToString (),
+            X = Pos.Right (lblSliderPosition) + 1,
+            Y = Pos.Top (lblSliderPosition)
         };
         };
-        frameView.Add (scrollPosition);
-
-        scrollPosition.ValueChanging += (s, e) =>
-                                        {
-                                            if (e.NewValue < 0)
-                                            {
-                                                e.Cancel = true;
-
-                                                return;
-                                            }
+        frameView.Add (scrollSliderPosition);
 
 
-                                            if (scroll.SliderPosition != e.NewValue)
-                                            {
-                                                scroll.SliderPosition = e.NewValue;
-                                            }
-
-                                            if (scroll.SliderPosition != e.NewValue)
-                                            {
-                                                e.Cancel = true;
-                                            }
-                                        };
+        var lblContentPosition = new Label
+        {
+            Text = "_ContentPosition:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
 
 
-        //var ckbKeepContentInAllViewport = new CheckBox
-        //{
-        //    Y = Pos.Bottom (scrollPosition), Text = "KeepContentInAllViewport",
-        //    CheckedState = scroll.KeepContentInAllViewport ? CheckState.Checked : CheckState.UnChecked
-        //};
-        //ckbKeepContentInAllViewport.CheckedStateChanging += (s, e) => scroll.KeepContentInAllViewport = e.NewValue == CheckState.Checked;
-        //view.Add (ckbKeepContentInAllViewport);
+        };
+        frameView.Add (lblContentPosition);
 
 
-        var lblSizeChanged = new Label
+        NumericUpDown<int> scrollContentPosition = new ()
         {
         {
-            Y = Pos.Bottom (scrollPosition) + 1
+            Value = scroll.GetSliderPosition (),
+            X = Pos.Right (lblContentPosition) + 1,
+            Y = Pos.Top (lblContentPosition)
         };
         };
-        frameView.Add (lblSizeChanged);
-
-        scroll.SizeChanged += (s, e) =>
-                              {
-                                  lblSizeChanged.Text = $"SizeChanged event - CurrentValue: {e.CurrentValue}";
+        frameView.Add (scrollContentPosition);
 
 
-                                  if (scrollSize.Value != e.CurrentValue)
-                                  {
-                                      scrollSize.Value = e.CurrentValue;
-                                  }
-                              };
-
-        var lblPosChanging = new Label
+        scrollContentPosition.ValueChanging += (s, e) =>
         {
         {
-            Y = Pos.Bottom (lblSizeChanged)
+            if (e.NewValue < 0)
+            {
+                e.Cancel = true;
+
+                return;
+            }
+
+            if (scroll.ContentPosition != e.NewValue)
+            {
+                scroll.ContentPosition = e.NewValue;
+            }
+
+            if (scroll.ContentPosition != e.NewValue)
+            {
+                e.Cancel = true;
+            }
         };
         };
-        frameView.Add (lblPosChanging);
 
 
-        scroll.SliderPositionChanging += (s, e) => { lblPosChanging.Text = $"PositionChanging event - CurrentValue: {e.CurrentValue}; NewValue: {e.NewValue}"; };
+        var lblOptions = new Label
+        {
+            Text = "_Options:",
+            TextAlignment = Alignment.End,
+            Y = Pos.Align (Alignment.Start, groupId: 1),
+            Width = Dim.Func (() => GetMaxLabelWidth (1))
+        };
+        frameView.Add (lblOptions);
 
 
-        var lblPositionChanged = new Label
+        var ckbShowPercent = new CheckBox
         {
         {
-            Y = Pos.Bottom (lblPosChanging)
+            Y = Pos.Top (lblOptions),
+            X = Pos.Right (lblOptions) + 1,
+            Text = "Sho_wPercent",
+            CheckedState = scroll.ShowPercent ? CheckState.Checked : CheckState.UnChecked
         };
         };
-        frameView.Add (lblPositionChanged);
+        ckbShowPercent.CheckedStateChanging += (s, e) => scroll.ShowPercent = e.NewValue == CheckState.Checked;
+        frameView.Add (ckbShowPercent);
 
 
-        scroll.SliderPositionChanged += (s, e) =>
-                                  {
-                                      lblPositionChanged.Text = $"PositionChanged event - CurrentValue: {e.CurrentValue}";
-                                      scrollPosition.Value = e.CurrentValue;
-                                  };
+        //var ckbKeepContentInAllViewport = new CheckBox
+        //{
+        //    X = Pos.Right (ckbShowScrollIndicator) + 1, Y = Pos.Bottom (scrollPosition), Text = "KeepContentInAllViewport",
+        //    CheckedState = Scroll.KeepContentInAllViewport ? CheckState.Checked : CheckState.UnChecked
+        //};
+        //ckbKeepContentInAllViewport.CheckedStateChanging += (s, e) => Scroll.KeepContentInAllViewport = e.NewValue == CheckState.Checked;
+        //view.Add (ckbKeepContentInAllViewport);
 
 
         var lblScrollFrame = new Label
         var lblScrollFrame = new Label
         {
         {
-            Y = Pos.Bottom (lblPositionChanged) + 1
+            Y = Pos.Bottom (lblOptions) + 1
         };
         };
         frameView.Add (lblScrollFrame);
         frameView.Add (lblScrollFrame);
 
 
@@ -248,21 +273,59 @@ public class ScrollDemo : Scenario
         };
         };
         frameView.Add (lblScrollContentSize);
         frameView.Add (lblScrollContentSize);
 
 
-
         scroll.SubviewsLaidOut += (s, e) =>
         scroll.SubviewsLaidOut += (s, e) =>
-                                 {
-                                     lblScrollFrame.Text = $"Scroll Frame: {scroll.Frame.ToString ()}";
-                                     lblScrollViewport.Text = $"Scroll Viewport: {scroll.Viewport.ToString ()}";
-                                     lblScrollContentSize.Text = $"Scroll ContentSize: {scroll.GetContentSize ().ToString ()}";
-                                 };
-
-        editor.Initialized += (s, e) =>
-                              {
-                                  scroll.Size = int.Max (app.GetContentSize ().Height * 2, app.GetContentSize ().Width * 2);
-                                  editor.ViewToEdit = scroll;
-                              };
-
-        app.Closed += (s, e) => View.Diagnostics = _diagnosticFlags;
+        {
+            lblScrollFrame.Text = $"Scroll Frame: {scroll.Frame.ToString ()}";
+            lblScrollViewport.Text = $"Scroll Viewport: {scroll.Viewport.ToString ()}";
+            lblScrollContentSize.Text = $"Scroll ContentSize: {scroll.GetContentSize ().ToString ()}";
+        };
+
+        EventLog eventLog = new ()
+        {
+            X = Pos.AnchorEnd () - 1,
+            Y = 0,
+            Height = Dim.Height (frameView),
+            BorderStyle = LineStyle.Single,
+            ViewToLog = scroll
+        };
+        app.Add (eventLog);
+        frameView.Width = Dim.Fill (Dim.Func (() => Math.Max (28, eventLog.Frame.Width + 1)));
+
+        app.Initialized += AppOnInitialized;
+
+
+        void AppOnInitialized (object sender, EventArgs e)
+        {
+            scroll.SizeChanged += (s, e) =>
+            {
+                eventLog.Log ($"SizeChanged: {e.CurrentValue}");
+
+                if (scrollSize.Value != e.CurrentValue)
+                {
+                    scrollSize.Value = e.CurrentValue;
+                }
+            };
+
+            scroll.SliderPositionChanged += (s, e) =>
+            {
+                eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}");
+                eventLog.Log ($"  ContentPosition: {scroll.ContentPosition}");
+                scrollSliderPosition.Text = e.CurrentValue.ToString ();
+            };
+
+            scroll.ContentPositionChanged += (s, e) =>
+                                            {
+                                                eventLog.Log ($"SliderPositionChanged: {e.CurrentValue}");
+                                                eventLog.Log ($"  ContentPosition: {scroll.ContentPosition}");
+                                                scrollContentPosition.Value = e.CurrentValue;
+                                            };
+
+            editor.Initialized += (s, e) =>
+            {
+                editor.ViewToEdit = scroll;
+            };
+
+        }
 
 
         Application.Run (app);
         Application.Run (app);
         app.Dispose ();
         app.Dispose ();

+ 6 - 13
UICatalog/Scenarios/TableEditor.cs

@@ -65,8 +65,8 @@ public class TableEditor : Scenario
              "Cuneiform Numbers and Punctuation"
              "Cuneiform Numbers and Punctuation"
             ),
             ),
         new (
         new (
-             (uint)(CharMap._maxCodePoint - 16),
-             (uint)CharMap._maxCodePoint,
+             (uint)(UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End) - 16),
+             (uint)UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End),
              "End"
              "End"
             ),
             ),
         new (0x0020, 0x007F, "Basic Latin"),
         new (0x0020, 0x007F, "Basic Latin"),
@@ -1533,17 +1533,10 @@ public class TableEditor : Scenario
                                   );
                                   );
     }
     }
 
 
-    private class UnicodeRange
+    public class UnicodeRange (uint start, uint end, string category)
     {
     {
-        public readonly string Category;
-        public readonly uint End;
-        public readonly uint Start;
-
-        public UnicodeRange (uint start, uint end, string category)
-        {
-            Start = start;
-            End = end;
-            Category = category;
-        }
+        public readonly string Category = category;
+        public readonly uint End = end;
+        public readonly uint Start = start;
     }
     }
 }
 }

+ 13 - 46
UnitTests/Views/ScrollBarTests.cs

@@ -52,7 +52,7 @@ public class ScrollBarTests
         Assert.False (scrollBar.CanFocus);
         Assert.False (scrollBar.CanFocus);
         Assert.Equal (Orientation.Vertical, scrollBar.Orientation);
         Assert.Equal (Orientation.Vertical, scrollBar.Orientation);
         Assert.Equal (0, scrollBar.Size);
         Assert.Equal (0, scrollBar.Size);
-        Assert.Equal (0, scrollBar.SliderPosition);
+        Assert.Equal (0, scrollBar.GetSliderPosition ());
         Assert.True (scrollBar.AutoHide);
         Assert.True (scrollBar.AutoHide);
     }
     }
 
 
@@ -82,43 +82,10 @@ public class ScrollBarTests
         };
         };
         super.Add (scrollBar);
         super.Add (scrollBar);
         scrollBar.Layout ();
         scrollBar.Layout ();
-        scrollBar.SliderPosition = 1;
+        scrollBar.ContentPosition = 1;
         scrollBar.Orientation = Orientation.Horizontal;
         scrollBar.Orientation = Orientation.Horizontal;
 
 
-        Assert.Equal (0, scrollBar.SliderPosition);
-    }
-
-
-    [Fact]
-    public void SliderPosition_Event_Cancelables ()
-    {
-        var changingCount = 0;
-        var changedCount = 0;
-        var scrollBar = new ScrollBar { };
-        scrollBar.Layout ();
-        scrollBar.Size = scrollBar.Viewport.Height * 2;
-        scrollBar.Layout ();
-
-        scrollBar.SliderPositionChanging += (s, e) =>
-                                         {
-                                             if (changingCount == 0)
-                                             {
-                                                 e.Cancel = true;
-                                             }
-
-                                             changingCount++;
-                                         };
-        scrollBar.SliderPositionChanged += (s, e) => changedCount++;
-
-        scrollBar.SliderPosition = 1;
-        Assert.Equal (0, scrollBar.SliderPosition);
-        Assert.Equal (1, changingCount);
-        Assert.Equal (0, changedCount);
-
-        scrollBar.SliderPosition = 1;
-        Assert.Equal (1, scrollBar.SliderPosition);
-        Assert.Equal (2, changingCount);
-        Assert.Equal (1, changedCount);
+        Assert.Equal (0, scrollBar.GetSliderPosition ());
     }
     }
 
 
 
 
@@ -179,10 +146,10 @@ public class ScrollBarTests
         Assert.Equal (30, scrollBar.Size);
         Assert.Equal (30, scrollBar.Size);
 
 
         scrollBar.KeepContentInAllViewport = false;
         scrollBar.KeepContentInAllViewport = false;
-        scrollBar.SliderPosition = 50;
-        Assert.Equal (scrollBar.SliderPosition, scrollBar.Size - 1);
-        Assert.Equal (scrollBar.SliderPosition, view.Viewport.Y);
-        Assert.Equal (29, scrollBar.SliderPosition);
+        scrollBar.ContentPosition = 50;
+        Assert.Equal (scrollBar.GetSliderPosition (), scrollBar.Size - 1);
+        Assert.Equal (scrollBar.GetSliderPosition (), view.Viewport.Y);
+        Assert.Equal (29, scrollBar.GetSliderPosition ());
         Assert.Equal (29, view.Viewport.Y);
         Assert.Equal (29, view.Viewport.Y);
 
 
         top.Dispose ();
         top.Dispose ();
@@ -198,7 +165,7 @@ public class ScrollBarTests
         var scrollBar = new ScrollBar
         var scrollBar = new ScrollBar
         {
         {
             X = 10, Y = 10, Width = orientation == Orientation.Vertical ? 1 : 10, Height = orientation == Orientation.Vertical ? 10 : 1, Size = 20,
             X = 10, Y = 10, Width = orientation == Orientation.Vertical ? 1 : 10, Height = orientation == Orientation.Vertical ? 10 : 1, Size = 20,
-            SliderPosition = 5, Orientation = orientation, KeepContentInAllViewport = true
+            ContentPosition = 5, Orientation = orientation, KeepContentInAllViewport = true
         };
         };
         var top = new Toplevel ();
         var top = new Toplevel ();
         top.Add (scrollBar);
         top.Add (scrollBar);
@@ -346,7 +313,7 @@ public class ScrollBarTests
 
 
         scrollBar.Size = sliderSize;
         scrollBar.Size = sliderSize;
         scrollBar.Layout ();
         scrollBar.Layout ();
-        scrollBar.SliderPosition = sliderPosition;
+        scrollBar.ContentPosition = sliderPosition;
 
 
         super.BeginInit ();
         super.BeginInit ();
         super.EndInit ();
         super.EndInit ();
@@ -392,10 +359,10 @@ public class ScrollBarTests
 
 
         top.Add (scrollBar);
         top.Add (scrollBar);
         RunState rs = Application.Begin (top);
         RunState rs = Application.Begin (top);
-        scrollBar.SliderPosition = 5;
+        scrollBar.ContentPosition = 5;
         Application.RunIteration (ref rs);
         Application.RunIteration (ref rs);
 
 
-        Assert.Equal (5, scrollBar.SliderPosition);
+        Assert.Equal (5, scrollBar.GetSliderPosition ());
         Assert.Equal (12, scrollBar.ContentPosition);
         Assert.Equal (12, scrollBar.ContentPosition);
         int initialPos = scrollBar.ContentPosition;
         int initialPos = scrollBar.ContentPosition;
 
 
@@ -433,10 +400,10 @@ public class ScrollBarTests
 
 
         top.Add (scrollBar);
         top.Add (scrollBar);
         RunState rs = Application.Begin (top);
         RunState rs = Application.Begin (top);
-        scrollBar.SliderPosition = 0;
+        scrollBar.ContentPosition = 0;
         Application.RunIteration (ref rs);
         Application.RunIteration (ref rs);
 
 
-        Assert.Equal (0, scrollBar.SliderPosition);
+        Assert.Equal (0, scrollBar.GetSliderPosition ());
         Assert.Equal (0, scrollBar.ContentPosition);
         Assert.Equal (0, scrollBar.ContentPosition);
         int initialPos = scrollBar.ContentPosition;
         int initialPos = scrollBar.ContentPosition;
 
 

+ 24 - 64
UnitTests/Views/ScrollTests.cs

@@ -21,7 +21,7 @@ public class ScrollTests
     }
     }
 
 
     [Fact]
     [Fact]
-    public void OnOrientationChanged_Sets_Position_To_0 ()
+    public void OnOrientationChanged_Sets_ContentPosition_To_0 ()
     {
     {
         View super = new View ()
         View super = new View ()
         {
         {
@@ -34,10 +34,10 @@ public class ScrollTests
         };
         };
         super.Add (scroll);
         super.Add (scroll);
         scroll.Layout ();
         scroll.Layout ();
-        scroll.SliderPosition = 1;
+        scroll.ContentPosition = 1;
         scroll.Orientation = Orientation.Horizontal;
         scroll.Orientation = Orientation.Horizontal;
 
 
-        Assert.Equal (0, scroll.SliderPosition);
+        Assert.Equal (0, scroll.ContentPosition);
     }
     }
 
 
 
 
@@ -224,7 +224,7 @@ public class ScrollTests
         Assert.False (scroll.CanFocus);
         Assert.False (scroll.CanFocus);
         Assert.Equal (Orientation.Vertical, scroll.Orientation);
         Assert.Equal (Orientation.Vertical, scroll.Orientation);
         Assert.Equal (0, scroll.Size);
         Assert.Equal (0, scroll.Size);
-        Assert.Equal (0, scroll.SliderPosition);
+        Assert.Equal (0, scroll.GetSliderPosition ());
     }
     }
 
 
     //[Fact]
     //[Fact]
@@ -324,7 +324,7 @@ public class ScrollTests
         top.Add (scroll);
         top.Add (scroll);
         RunState rs = Application.Begin (top);
         RunState rs = Application.Begin (top);
 
 
-        Assert.Equal (0, scroll.SliderPosition);
+        Assert.Equal (0, scroll.GetSliderPosition ());
         Assert.Equal (0, scroll.ContentPosition);
         Assert.Equal (0, scroll.ContentPosition);
 
 
         Application.RaiseMouseEvent (new ()
         Application.RaiseMouseEvent (new ()
@@ -377,10 +377,10 @@ public class ScrollTests
 
 
         top.Add (scroll);
         top.Add (scroll);
         RunState rs = Application.Begin (top);
         RunState rs = Application.Begin (top);
-        scroll.SliderPosition = 5;
+        scroll.ContentPosition = 5;
         Application.RunIteration (ref rs);
         Application.RunIteration (ref rs);
 
 
-        Assert.Equal (5, scroll.SliderPosition);
+        Assert.Equal (5, scroll.GetSliderPosition ());
         Assert.Equal (10, scroll.ContentPosition);
         Assert.Equal (10, scroll.ContentPosition);
 
 
         Application.RaiseMouseEvent (new ()
         Application.RaiseMouseEvent (new ()
@@ -390,7 +390,7 @@ public class ScrollTests
         });
         });
         Application.RunIteration (ref rs);
         Application.RunIteration (ref rs);
 
 
-        Assert.Equal (0, scroll.SliderPosition);
+        Assert.Equal (0, scroll.GetSliderPosition ());
         Assert.Equal (0, scroll.ContentPosition);
         Assert.Equal (0, scroll.ContentPosition);
 
 
         Application.ResetState (true);
         Application.ResetState (true);
@@ -421,42 +421,10 @@ public class ScrollTests
         scroll.Size = scrollSize;
         scroll.Size = scrollSize;
         super.Layout ();
         super.Layout ();
 
 
-        scroll.SliderPosition = scrollPosition;
+        scroll.ContentPosition = scrollPosition;
         super.Layout ();
         super.Layout ();
 
 
-        Assert.True (scroll.SliderPosition <= scrollSize);
-    }
-
-    [Fact]
-    public void SliderPosition_Event_Cancelables ()
-    {
-        var changingCount = 0;
-        var changedCount = 0;
-        var scroll = new Scroll { };
-        scroll.Layout ();
-        scroll.Size = scroll.Viewport.Height * 2;
-        scroll.Layout ();
-
-        scroll.SliderPositionChanging += (s, e) =>
-                                   {
-                                       if (changingCount == 0)
-                                       {
-                                           e.Cancel = true;
-                                       }
-
-                                       changingCount++;
-                                   };
-        scroll.SliderPositionChanged += (s, e) => changedCount++;
-
-        scroll.SliderPosition = 1;
-        Assert.Equal (0, scroll.SliderPosition);
-        Assert.Equal (1, changingCount);
-        Assert.Equal (0, changedCount);
-
-        scroll.SliderPosition = 1;
-        Assert.Equal (1, scroll.SliderPosition);
-        Assert.Equal (2, changingCount);
-        Assert.Equal (1, changedCount);
+        Assert.True (scroll.GetSliderPosition () <= scrollSize);
     }
     }
 
 
     [Fact]
     [Fact]
@@ -466,60 +434,52 @@ public class ScrollTests
         var cancel = false;
         var cancel = false;
         var changed = 0;
         var changed = 0;
         var scroll = new Scroll { Height = 10, Size = 20 };
         var scroll = new Scroll { Height = 10, Size = 20 };
-        scroll.SliderPositionChanging += Scroll_PositionChanging;
         scroll.SliderPositionChanged += Scroll_PositionChanged;
         scroll.SliderPositionChanged += Scroll_PositionChanged;
 
 
         Assert.Equal (Orientation.Vertical, scroll.Orientation);
         Assert.Equal (Orientation.Vertical, scroll.Orientation);
         scroll.Layout ();
         scroll.Layout ();
         Assert.Equal (new (0, 0, 1, 10), scroll.Viewport);
         Assert.Equal (new (0, 0, 1, 10), scroll.Viewport);
-        Assert.Equal (0, scroll.SliderPosition);
+        Assert.Equal (0, scroll.GetSliderPosition ());
         Assert.Equal (0, changing);
         Assert.Equal (0, changing);
         Assert.Equal (0, changed);
         Assert.Equal (0, changed);
 
 
-        scroll.SliderPosition = 0;
-        Assert.Equal (0, scroll.SliderPosition);
+        scroll.ContentPosition = 0;
+        Assert.Equal (0, scroll.GetSliderPosition ());
         Assert.Equal (0, changing);
         Assert.Equal (0, changing);
         Assert.Equal (0, changed);
         Assert.Equal (0, changed);
 
 
-        scroll.SliderPosition = 1;
-        Assert.Equal (1, scroll.SliderPosition);
+        scroll.ContentPosition = 1;
+        Assert.Equal (1, scroll.GetSliderPosition ());
         Assert.Equal (1, changing);
         Assert.Equal (1, changing);
         Assert.Equal (1, changed);
         Assert.Equal (1, changed);
 
 
         Reset ();
         Reset ();
         cancel = true;
         cancel = true;
-        scroll.SliderPosition = 2;
-        Assert.Equal (1, scroll.SliderPosition);
+        scroll.ContentPosition = 2;
+        Assert.Equal (1, scroll.GetSliderPosition ());
         Assert.Equal (1, changing);
         Assert.Equal (1, changing);
         Assert.Equal (0, changed);
         Assert.Equal (0, changed);
 
 
         Reset ();
         Reset ();
-        scroll.SliderPosition = 10;
-        Assert.Equal (5, scroll.SliderPosition);
+        scroll.ContentPosition = 10;
+        Assert.Equal (5, scroll.GetSliderPosition ());
         Assert.Equal (1, changing);
         Assert.Equal (1, changing);
         Assert.Equal (1, changed);
         Assert.Equal (1, changed);
 
 
         Reset ();
         Reset ();
-        scroll.SliderPosition = 11;
-        Assert.Equal (5, scroll.SliderPosition);
+        scroll.ContentPosition = 11;
+        Assert.Equal (5, scroll.GetSliderPosition ());
         Assert.Equal (1, changing);
         Assert.Equal (1, changing);
         Assert.Equal (1, changed);
         Assert.Equal (1, changed);
 
 
         Reset ();
         Reset ();
-        scroll.SliderPosition = 0;
-        Assert.Equal (0, scroll.SliderPosition);
+        scroll.ContentPosition = 0;
+        Assert.Equal (0, scroll.GetSliderPosition ());
         Assert.Equal (1, changing);
         Assert.Equal (1, changing);
         Assert.Equal (1, changed);
         Assert.Equal (1, changed);
 
 
-        scroll.SliderPositionChanging -= Scroll_PositionChanging;
         scroll.SliderPositionChanged -= Scroll_PositionChanged;
         scroll.SliderPositionChanged -= Scroll_PositionChanged;
 
 
-        void Scroll_PositionChanging (object sender, CancelEventArgs<int> e)
-        {
-            changing++;
-            e.Cancel = cancel;
-        }
-
         void Scroll_PositionChanged (object sender, EventArgs<int> e) { changed++; }
         void Scroll_PositionChanged (object sender, EventArgs<int> e) { changed++; }
 
 
         void Reset ()
         void Reset ()
@@ -625,7 +585,7 @@ public class ScrollTests
 
 
         scroll.Size = sliderSize;
         scroll.Size = sliderSize;
         scroll.Layout ();
         scroll.Layout ();
-        scroll.SliderPosition = sliderPosition;
+        scroll.ContentPosition = sliderPosition;
 
 
         super.BeginInit ();
         super.BeginInit ();
         super.EndInit ();
         super.EndInit ();