Browse Source

Create a Scroll class with unit tests and use case.

BDisp 1 year ago
parent
commit
478c93d985
3 changed files with 1370 additions and 0 deletions
  1. 373 0
      Terminal.Gui/Views/Scroll.cs
  2. 195 0
      UICatalog/Scenarios/ScrollDemo.cs
  3. 802 0
      UnitTests/Views/ScrollTests.cs

+ 373 - 0
Terminal.Gui/Views/Scroll.cs

@@ -0,0 +1,373 @@
+//
+// Scroll.cs: Vertical or horizontal scroll
+//
+// Author: BDisp
+//
+// Licensed under the MIT license
+//
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Represents the "inside part" of a scroll bar, minus the arrows.
+/// </summary>
+public class Scroll : View
+{
+    /// <inheritdoc/>
+    public Scroll ()
+    {
+        WantContinuousButtonPressed = true;
+        ClearOnVisibleFalse = false;
+        CanFocus = false;
+        Orientation = Orientation.Vertical;
+
+        _sliderContainer = new () { Width = Dim.Fill (), Height = Dim.Fill (), WantContinuousButtonPressed = true };
+        _slider = new () { Id = "slider" };
+        _sliderContainer.Add (_slider);
+        Add (_sliderContainer);
+
+        Added += Scroll_Added;
+        Removed += Scroll_Removed;
+        Initialized += Scroll_Initialized;
+        _sliderContainer.DrawContent += SubViews_DrawContent;
+        _sliderContainer.MouseEvent += SliderContainer_MouseEvent;
+        _slider.DrawContent += SubViews_DrawContent;
+        _slider.MouseEvent += Slider_MouseEvent;
+    }
+
+    private readonly View _sliderContainer;
+    private readonly View _slider;
+    private int _lastLocation = -1;
+
+    private Orientation _orientation;
+    private int _size;
+    private int _position;
+
+    private bool _wasSliderMouse;
+
+    /// <summary>
+    ///     Determines the Orientation of the scroll.
+    /// </summary>
+    public Orientation Orientation
+    {
+        get => _orientation;
+        set
+        {
+            _orientation = value;
+            SetWidthHeight ();
+        }
+    }
+
+    /// <summary>
+    ///     The position, relative to <see cref="Size"/>, to set the scrollbar at.
+    /// </summary>
+    public int Position
+    {
+        get => _position;
+        set
+        {
+            int barSize = Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
+
+            if (value < 0 || (value > 0 && value + barSize > Size))
+            {
+                return;
+            }
+
+            StateEventArgs<int> args = OnPositionChanging (_position, value);
+
+            if (args.Cancel)
+            {
+                return;
+            }
+
+            int oldPos = _position;
+            _position = value;
+            OnPositionChanged (oldPos);
+
+            if (!_wasSliderMouse)
+            {
+                SetWidthHeight ();
+            }
+        }
+    }
+
+    /// <summary>This event is raised when the position on the scrollbar has changed.</summary>
+    public event EventHandler<StateEventArgs<int>> PositionChanged;
+
+    /// <summary>This event is raised when the position on the scrollbar is changing.</summary>
+    public event EventHandler<StateEventArgs<int>> PositionChanging;
+
+    /// <summary>
+    ///     The size of content the scroll represents.
+    /// </summary>
+    public int Size
+    {
+        get => _size;
+        set
+        {
+            int oldSize = _size;
+            _size = value;
+            OnSizeChanged (oldSize);
+            SetWidthHeight ();
+        }
+    }
+
+    /// <summary>This event is raised when the size of the scroll has changed.</summary>
+    public event EventHandler<StateEventArgs<int>> SizeChanged;
+
+    /// <inheritdoc/>
+    protected override void Dispose (bool disposing)
+    {
+        Added -= Scroll_Added;
+        Initialized -= Scroll_Initialized;
+        _sliderContainer.DrawContent -= SubViews_DrawContent;
+        _sliderContainer.MouseEvent -= SliderContainer_MouseEvent;
+        _slider.DrawContent -= SubViews_DrawContent;
+        _slider.MouseEvent -= Slider_MouseEvent;
+
+        base.Dispose (disposing);
+    }
+
+    /// <summary>Virtual method to invoke the <see cref="PositionChanged"/> event handler.</summary>
+    protected virtual void OnPositionChanged (int oldPos) { PositionChanged?.Invoke (this, new (oldPos, Position)); }
+
+    /// <summary>Virtual method to invoke the cancelable <see cref="PositionChanging"/> event handler.</summary>
+    protected virtual StateEventArgs<int> OnPositionChanging (int oldPos, int newPos)
+    {
+        StateEventArgs<int> args = new (oldPos, newPos);
+        PositionChanging?.Invoke (this, args);
+
+        return args;
+    }
+
+    /// <summary>Virtual method to invoke the <see cref="SizeChanged"/> event handler.</summary>
+    protected void OnSizeChanged (int oldSize) { SizeChanged?.Invoke (this, new (oldSize, Size)); }
+
+    private int GetPositionFromSliderLocation (int location)
+    {
+        if (Frame.Height == 0 || Frame.Width == 0)
+        {
+            return 0;
+        }
+
+        int barSize = Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
+
+        return Math.Min (location * Size / barSize, Size - barSize);
+    }
+
+    private (int Location, int Dimension) GetSliderLocationDimensionFromPosition ()
+    {
+        if (Frame.Height == 0 || Frame.Width == 0)
+        {
+            return new (0, 0);
+        }
+
+        int barSize = Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
+        int location;
+        int dimension;
+
+        if (Size > 0)
+        {
+            dimension = Math.Min (Math.Max (barSize * barSize / Size, 1), barSize);
+
+            // Ensure the Position is valid
+            if (Position > 0 && Position + barSize > Size)
+            {
+                Position = Size - barSize;
+            }
+
+            location = Math.Min (Position * barSize / Size, barSize - dimension);
+
+            if (Position == Size - barSize && location + dimension < barSize)
+            {
+                location = barSize - dimension;
+            }
+        }
+        else
+        {
+            location = 0;
+            dimension = barSize;
+        }
+
+        return new (location, dimension);
+    }
+
+    private void Parent_LayoutComplete (object sender, LayoutEventArgs e)
+    {
+        if (!_wasSliderMouse)
+        {
+            SetWidthHeight ();
+        }
+        else
+        {
+            _wasSliderMouse = false;
+        }
+    }
+
+    private void Parent_MouseEnter (object sender, MouseEventEventArgs e) { OnMouseEnter (e.MouseEvent); }
+
+    private void Parent_MouseLeave (object sender, MouseEventEventArgs e) { OnMouseLeave (e.MouseEvent); }
+
+    private void Scroll_Added (object sender, SuperViewChangedEventArgs e)
+    {
+        View parent = e.Parent is Adornment adornment ? adornment.Parent : e.Parent;
+
+        parent.LayoutComplete += Parent_LayoutComplete;
+        parent.MouseEnter += Parent_MouseEnter;
+        parent.MouseLeave += Parent_MouseLeave;
+    }
+
+    private void Scroll_Initialized (object sender, EventArgs e) { SetWidthHeight (); }
+
+    private void Scroll_Removed (object sender, SuperViewChangedEventArgs e)
+    {
+        if (e.Parent is { })
+        {
+            View parent = e.Parent is Adornment adornment ? adornment.Parent : e.Parent;
+
+            parent.LayoutComplete -= Parent_LayoutComplete;
+            parent.MouseEnter -= Parent_MouseEnter;
+            parent.MouseLeave -= Parent_MouseLeave;
+        }
+    }
+
+    private static void SetColorSchemeWithSuperview (View view)
+    {
+        if (view.SuperView is { })
+        {
+            View parent = view.SuperView is Adornment adornment ? adornment.Parent : view.SuperView;
+
+            if (view.Id == "slider")
+            {
+                view.ColorScheme = new () { Normal = new (parent.ColorScheme.Normal.Foreground, parent.ColorScheme.Normal.Foreground) };
+            }
+            else
+            {
+                view.ColorScheme = parent.ColorScheme;
+            }
+        }
+    }
+
+    private void SetSliderText ()
+    {
+        _sliderContainer.TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
+
+        _sliderContainer.Text = string.Concat (
+                                               Enumerable.Repeat (
+                                                                  Glyphs.Stipple.ToString (),
+                                                                  Orientation == Orientation.Vertical
+                                                                      ? _sliderContainer.Frame.Height
+                                                                      : _sliderContainer.Frame.Width));
+        _slider.TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
+
+        _slider.Text = string.Concat (
+                                      Enumerable.Repeat (
+                                                         Glyphs.ContinuousMeterSegment.ToString (),
+                                                         Orientation == Orientation.Vertical
+                                                             ? _sliderContainer.Frame.Height
+                                                             : _slider.Frame.Width));
+    }
+
+    private void SetWidthHeight ()
+    {
+        if (Orientation == Orientation.Vertical)
+        {
+            Width = 1;
+        }
+        else
+        {
+            Height = 1;
+        }
+
+        if (!IsInitialized)
+        {
+            return;
+        }
+
+        _sliderContainer.Width = Orientation == Orientation.Vertical ? 1 : Dim.Fill ();
+        _sliderContainer.Height = Orientation == Orientation.Vertical ? Dim.Fill () : 1;
+
+        (int Location, int Dimension) slider = GetSliderLocationDimensionFromPosition ();
+        _slider.X = Orientation == Orientation.Vertical ? 0 : slider.Location;
+        _slider.Y = Orientation == Orientation.Vertical ? slider.Location : 0;
+        _slider.Width = Orientation == Orientation.Vertical ? 1 : slider.Dimension;
+        _slider.Height = Orientation == Orientation.Vertical ? slider.Dimension : 1;
+
+        SetSliderText ();
+    }
+
+    private void Slider_MouseEvent (object sender, MouseEventEventArgs e)
+    {
+        MouseEvent me = e.MouseEvent;
+        int location = Orientation == Orientation.Vertical ? me.Position.Y : me.Position.X;
+        int barSize = Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
+        int offset = _lastLocation > -1 ? location - _lastLocation : 0;
+
+        if (me.Flags == MouseFlags.Button1Pressed)
+        {
+            if (Application.MouseGrabView != sender as View)
+            {
+                Application.GrabMouse (sender as View);
+                _lastLocation = location;
+            }
+        }
+        else if (me.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
+        {
+            if (Orientation == Orientation.Vertical)
+            {
+                if (_slider.Frame.Y + offset >= 0 && _slider.Frame.Y + offset + _slider.Frame.Height <= barSize)
+                {
+                    _wasSliderMouse = true;
+                    _slider.Y = _slider.Frame.Y + offset;
+                    Position = GetPositionFromSliderLocation (_slider.Frame.Y);
+                }
+            }
+            else
+            {
+                if (_slider.Frame.X + offset >= 0 && _slider.Frame.X + offset + _slider.Frame.Width <= barSize)
+                {
+                    _wasSliderMouse = true;
+                    _slider.X = _slider.Frame.X + offset;
+                    Position = GetPositionFromSliderLocation (_slider.Frame.X);
+                }
+            }
+        }
+        else if (me.Flags == MouseFlags.Button1Released)
+        {
+            _lastLocation = -1;
+
+            if (Application.MouseGrabView == sender as View)
+            {
+                Application.UngrabMouse ();
+            }
+        }
+        else
+        {
+            return;
+        }
+
+        e.Handled = true;
+    }
+
+    private void SliderContainer_MouseEvent (object sender, MouseEventEventArgs e)
+    {
+        MouseEvent me = e.MouseEvent;
+        int location = Orientation == Orientation.Vertical ? me.Position.Y : me.Position.X;
+        int barSize = Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
+
+        (int topLeft, int bottomRight) sliderPos = _orientation == Orientation.Vertical
+                                                       ? new (_slider.Frame.Y, _slider.Frame.Bottom - 1)
+                                                       : new (_slider.Frame.X, _slider.Frame.Right - 1);
+
+        if (me.Flags == MouseFlags.Button1Pressed && location < sliderPos.topLeft)
+        {
+            Position = Math.Max (Position - barSize, 0);
+        }
+        else if (me.Flags == MouseFlags.Button1Pressed && location > sliderPos.bottomRight)
+        {
+            Position = Math.Min (Position + barSize, Size - barSize);
+        }
+    }
+
+    private void SubViews_DrawContent (object sender, DrawEventArgs e) { SetColorSchemeWithSuperview (sender as View); }
+}

+ 195 - 0
UICatalog/Scenarios/ScrollDemo.cs

@@ -0,0 +1,195 @@
+using Terminal.Gui;
+
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("Scroll Demo", "Demonstrates using Scroll view.")]
+[ScenarioCategory ("Drawing")]
+[ScenarioCategory ("Scrolling")]
+public class ScrollDemo : Scenario
+{
+    private ViewDiagnosticFlags _diagnosticFlags;
+
+    public override void Main ()
+    {
+        Application.Init ();
+
+        _diagnosticFlags = View.Diagnostics;
+
+        Window app = new ()
+        {
+            Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}"
+        };
+
+        var editor = new Adornments.AdornmentsEditor ();
+        app.Add (editor);
+
+        var view = new FrameView
+        {
+            Title = "Demo View",
+            X = Pos.Right (editor),
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            ColorScheme = Colors.ColorSchemes ["Base"]
+        };
+        app.Add (view);
+
+        var scroll = new Scroll
+        {
+            X = Pos.AnchorEnd (),
+            Width = Dim.Fill (),
+            Height = Dim.Fill ()
+        };
+        view.Add (scroll);
+
+        var rgOrientation = new RadioGroup
+        {
+            RadioLabels = ["Vertical", "Horizontal"],
+            Orientation = Orientation.Horizontal
+        };
+        view.Add (rgOrientation);
+
+        rgOrientation.SelectedItemChanged += (s, e) =>
+                                             {
+                                                 if (e.SelectedItem == e.PreviousSelectedItem)
+                                                 {
+                                                     return;
+                                                 }
+
+                                                 if (rgOrientation.SelectedItem == 0)
+                                                 {
+                                                     scroll.Orientation = Orientation.Vertical;
+                                                     scroll.X = Pos.AnchorEnd ();
+                                                     scroll.Height = Dim.Fill ();
+                                                     scroll.Size /= 3;
+                                                 }
+                                                 else
+                                                 {
+                                                     scroll.Orientation = Orientation.Horizontal;
+                                                     scroll.Y = Pos.AnchorEnd ();
+                                                     scroll.Width = Dim.Fill ();
+                                                     scroll.Size *= 3;
+                                                 }
+                                             };
+
+        var lblSize = new Label
+        {
+            Y = Pos.Bottom (rgOrientation),
+            Text = "Size:"
+        };
+        view.Add (lblSize);
+
+        Buttons.NumericUpDown<int> scrollSize = new Buttons.NumericUpDown<int>
+        {
+            Value = scroll.Size,
+            X = Pos.Right (lblSize) + 1,
+            Y = Pos.Top (lblSize)
+        };
+        view.Add (scrollSize);
+
+        scrollSize.ValueChanging += (s, e) =>
+                                    {
+                                        if (e.NewValue < 0)
+                                        {
+                                            e.Cancel = true;
+
+                                            return;
+                                        }
+
+                                        if (scroll.Size != e.NewValue)
+                                        {
+                                            scroll.Size = e.NewValue;
+                                        }
+                                    };
+
+        var lblPosition = new Label
+        {
+            Y = Pos.Bottom (lblSize),
+            Text = "Position:"
+        };
+        view.Add (lblPosition);
+
+        Buttons.NumericUpDown<int> scrollPosition = new Buttons.NumericUpDown<int>
+        {
+            Value = scroll.Position,
+            X = Pos.Right (lblPosition) + 1,
+            Y = Pos.Top (lblPosition)
+        };
+        view.Add (scrollPosition);
+
+        scrollPosition.ValueChanging += (s, e) =>
+                                        {
+                                            if (e.NewValue < 0)
+                                            {
+                                                e.Cancel = true;
+
+                                                return;
+                                            }
+
+                                            if (scroll.Position != e.NewValue)
+                                            {
+                                                scroll.Position = e.NewValue;
+                                            }
+
+                                            if (scroll.Position != e.NewValue)
+                                            {
+                                                e.Cancel = true;
+                                            }
+                                        };
+
+        var lblSizeChanged = new Label
+        {
+            Y = Pos.Bottom (lblPosition) + 1
+        };
+        view.Add (lblSizeChanged);
+
+        scroll.SizeChanged += (s, e) =>
+                              {
+                                  lblSizeChanged.Text = $"SizeChanged event - OldValue: {e.OldValue}; NewValue: {e.NewValue}";
+
+                                  if (scrollSize.Value != e.NewValue)
+                                  {
+                                      scrollSize.Value = e.NewValue;
+                                  }
+                              };
+
+        var lblPosChanging = new Label
+        {
+            Y = Pos.Bottom (lblSizeChanged)
+        };
+        view.Add (lblPosChanging);
+
+        scroll.PositionChanging += (s, e) => { lblPosChanging.Text = $"PositionChanging event - OldValue: {e.OldValue}; NewValue: {e.NewValue}"; };
+
+        var lblPositionChanged = new Label
+        {
+            Y = Pos.Bottom (lblPosChanging)
+        };
+        view.Add (lblPositionChanged);
+
+        scroll.PositionChanged += (s, e) =>
+                                  {
+                                      lblPositionChanged.Text = $"PositionChanged event - OldValue: {e.OldValue}; NewValue: {e.NewValue}";
+                                      scrollPosition.Value = e.NewValue;
+                                  };
+
+        var lblScrollFrame = new Label
+        {
+            Y = Pos.Bottom (lblPositionChanged) + 1
+        };
+        view.Add (lblScrollFrame);
+
+        scroll.LayoutComplete += (s, e) => lblScrollFrame.Text = $"Scroll Frame: {scroll.Frame.ToString ()}";
+
+        editor.Initialized += (s, e) =>
+                              {
+                                  scroll.Size = 40;
+                                  editor.ViewToEdit = view;
+                              };
+
+        app.Closed += (s, e) => View.Diagnostics = _diagnosticFlags;
+
+        Application.Run (app);
+        app.Dispose ();
+        Application.Shutdown ();
+    }
+}

+ 802 - 0
UnitTests/Views/ScrollTests.cs

@@ -0,0 +1,802 @@
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.ViewsTests;
+
+public class ScrollTests
+{
+    public ScrollTests (ITestOutputHelper output) { _output = output; }
+    private readonly ITestOutputHelper _output;
+
+    [Theory]
+    [AutoInitShutdown]
+    [InlineData (
+                    20,
+                    @"
+█
+█
+█
+█
+█
+░
+░
+░
+░
+░",
+                    @"
+░
+░
+█
+█
+█
+█
+█
+░
+░
+░",
+                    @"
+░
+░
+░
+░
+░
+█
+█
+█
+█
+█",
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    @"
+█████░░░░░",
+                    @"
+░░█████░░░",
+                    @"
+░░░░░█████",
+                    @"
+░░██░░░░░░")]
+    [InlineData (
+                    40,
+                    @"
+█
+█
+░
+░
+░
+░
+░
+░
+░
+░",
+                    @"
+░
+█
+█
+░
+░
+░
+░
+░
+░
+░",
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    @"
+░
+█
+░
+░
+░
+░
+░
+░
+░
+░",
+                    @"
+██░░░░░░░░",
+                    @"
+░██░░░░░░░",
+                    @"
+░░██░░░░░░",
+                    @"
+░█░░░░░░░░")]
+    public void Changing_Position_Size_Orientation_Draws_Correctly (
+        int size,
+        string firstVertExpected,
+        string middleVertExpected,
+        string endVertExpected,
+        string sizeVertExpected,
+        string firstHoriExpected,
+        string middleHoriExpected,
+        string endHoriExpected,
+        string sizeHoriExpected
+    )
+    {
+        var scroll = new Scroll
+        {
+            Orientation = Orientation.Vertical,
+            Size = size,
+            Height = 10
+        };
+        var top = new Toplevel ();
+        top.Add (scroll);
+        Application.Begin (top);
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (firstVertExpected, _output);
+
+        scroll.Position = 4;
+        Application.Refresh ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (middleVertExpected, _output);
+
+        scroll.Position = 10;
+        Application.Refresh ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (endVertExpected, _output);
+
+        scroll.Size = size * 2;
+        Application.Refresh ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (sizeVertExpected, _output);
+
+        scroll.Orientation = Orientation.Horizontal;
+        scroll.Width = 10;
+        scroll.Position = 0;
+        scroll.Size = size;
+        Application.Refresh ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (firstHoriExpected, _output);
+
+        scroll.Position = 4;
+        Application.Refresh ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (middleHoriExpected, _output);
+
+        scroll.Position = 10;
+        Application.Refresh ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (endHoriExpected, _output);
+
+        scroll.Size = size * 2;
+        Application.Refresh ();
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (sizeHoriExpected, _output);
+    }
+
+    [Fact]
+    public void Constructor_Defaults ()
+    {
+        var scroll = new Scroll ();
+        Assert.True (scroll.WantContinuousButtonPressed);
+        Assert.False (scroll.ClearOnVisibleFalse);
+        Assert.False (scroll.CanFocus);
+        Assert.Equal (Orientation.Vertical, scroll.Orientation);
+        Assert.Equal (0, scroll.Size);
+        Assert.Equal (0, scroll.Position);
+    }
+
+    [Theory]
+    [AutoInitShutdown]
+    [InlineData (
+                    Orientation.Vertical,
+                    20,
+                    10,
+                    4,
+                    @"
+░
+░
+░
+░
+░
+█
+█
+█
+█
+█",
+                    0,
+                    @"
+█
+█
+█
+█
+█
+░
+░
+░
+░
+░")]
+    [InlineData (
+                    Orientation.Vertical,
+                    40,
+                    10,
+                    5,
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    20,
+                    @"
+░
+░
+░
+░
+░
+█
+█
+░
+░
+░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    20,
+                    10,
+                    4,
+                    @"
+░░░░░█████",
+                    0,
+                    @"
+█████░░░░░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    40,
+                    10,
+                    5,
+                    @"
+░░██░░░░░░",
+                    20,
+                    @"
+░░░░░██░░░")]
+    public void Mouse_On_The_Container (Orientation orientation, int size, int position, int location, string output, int expectedPos, string expectedOut)
+    {
+        var scroll = new Scroll { Width = 10, Height = 10, Orientation = orientation, Size = size, Position = position };
+        var top = new Toplevel ();
+        top.Add (scroll);
+        Application.Begin (top);
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (output, _output);
+
+        Application.OnMouseEvent (
+                                  new ()
+                                  {
+                                      Position = orientation == Orientation.Vertical ? new (0, location) : new Point (location, 0),
+                                      Flags = MouseFlags.Button1Pressed
+                                  });
+        Assert.Equal (expectedPos, scroll.Position);
+
+        Application.Refresh ();
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (expectedOut, _output);
+    }
+
+    [Theory]
+    [AutoInitShutdown]
+    [InlineData (
+                    Orientation.Vertical,
+                    20,
+                    10,
+                    5,
+                    5,
+                    @"
+░
+░
+░
+░
+░
+█
+█
+█
+█
+█",
+                    MouseFlags.Button1Pressed,
+                    10,
+                    @"
+░
+░
+░
+░
+░
+█
+█
+█
+█
+█")]
+    [InlineData (
+                    Orientation.Vertical,
+                    40,
+                    10,
+                    3,
+                    3,
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    MouseFlags.Button1Pressed,
+                    10,
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    20,
+                    10,
+                    5,
+                    5,
+                    @"
+░░░░░█████",
+                    MouseFlags.Button1Pressed,
+                    10,
+                    @"
+░░░░░█████")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    40,
+                    10,
+                    3,
+                    3,
+                    @"
+░░██░░░░░░",
+                    MouseFlags.Button1Pressed,
+                    10,
+                    @"
+░░██░░░░░░")]
+    [InlineData (
+                    Orientation.Vertical,
+                    20,
+                    10,
+                    5,
+                    4,
+                    @"
+░
+░
+░
+░
+░
+█
+█
+█
+█
+█",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    8,
+                    @"
+░
+░
+░
+░
+█
+█
+█
+█
+█
+░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    20,
+                    10,
+                    5,
+                    4,
+                    @"
+░░░░░█████",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    8,
+                    @"
+░░░░█████░")]
+    [InlineData (
+                    Orientation.Vertical,
+                    20,
+                    10,
+                    5,
+                    6,
+                    @"
+░
+░
+░
+░
+░
+█
+█
+█
+█
+█",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    10,
+                    @"
+░
+░
+░
+░
+░
+█
+█
+█
+█
+█")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    20,
+                    10,
+                    5,
+                    6,
+                    @"
+░░░░░█████",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    10,
+                    @"
+░░░░░█████")]
+    [InlineData (
+                    Orientation.Vertical,
+                    40,
+                    10,
+                    2,
+                    1,
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    4,
+                    @"
+░
+█
+█
+░
+░
+░
+░
+░
+░
+░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    40,
+                    10,
+                    2,
+                    1,
+                    @"
+░░██░░░░░░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    4,
+                    @"
+░██░░░░░░░")]
+    [InlineData (
+                    Orientation.Vertical,
+                    40,
+                    10,
+                    3,
+                    4,
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    12,
+                    @"
+░
+░
+░
+█
+█
+░
+░
+░
+░
+░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    40,
+                    10,
+                    3,
+                    4,
+                    @"
+░░██░░░░░░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    12,
+                    @"
+░░░██░░░░░")]
+    [InlineData (
+                    Orientation.Vertical,
+                    40,
+                    10,
+                    2,
+                    3,
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    12,
+                    @"
+░
+░
+░
+█
+█
+░
+░
+░
+░
+░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    40,
+                    10,
+                    2,
+                    3,
+                    @"
+░░██░░░░░░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    12,
+                    @"
+░░░██░░░░░")]
+    [InlineData (
+                    Orientation.Vertical,
+                    40,
+                    10,
+                    2,
+                    4,
+                    @"
+░
+░
+█
+█
+░
+░
+░
+░
+░
+░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    16,
+                    @"
+░
+░
+░
+░
+█
+█
+░
+░
+░
+░")]
+    [InlineData (
+                    Orientation.Horizontal,
+                    40,
+                    10,
+                    2,
+                    4,
+                    @"
+░░██░░░░░░",
+                    MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition,
+                    16,
+                    @"
+░░░░██░░░░")]
+    public void Mouse_On_The_Slider (
+        Orientation orientation,
+        int size,
+        int position,
+        int startLocation,
+        int endLocation,
+        string output,
+        MouseFlags mouseFlags,
+        int expectedPos,
+        string expectedOut
+    )
+    {
+        var scroll = new Scroll { Width = 10, Height = 10, Orientation = orientation, Size = size, Position = position };
+        var top = new Toplevel ();
+        top.Add (scroll);
+        Application.Begin (top);
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (output, _output);
+
+        Assert.Null (Application.MouseGrabView);
+
+        if (mouseFlags.HasFlag (MouseFlags.ReportMousePosition))
+        {
+            MouseFlags mf = mouseFlags & ~MouseFlags.ReportMousePosition;
+
+            Application.OnMouseEvent (
+                                      new ()
+                                      {
+                                          Position = orientation == Orientation.Vertical ? new (0, startLocation) : new (startLocation, 0),
+                                          Flags = mf
+                                      });
+
+            Application.OnMouseEvent (
+                                      new ()
+                                      {
+                                          Position = orientation == Orientation.Vertical ? new (0, endLocation) : new (endLocation, 0),
+                                          Flags = mouseFlags
+                                      });
+        }
+        else
+        {
+            Assert.Equal (startLocation, endLocation);
+
+            Application.OnMouseEvent (
+                                      new ()
+                                      {
+                                          Position = orientation == Orientation.Vertical ? new (0, startLocation) : new (startLocation, 0),
+                                          Flags = mouseFlags
+                                      });
+        }
+
+        Assert.Equal ("slider", Application.MouseGrabView?.Id);
+        Assert.Equal (expectedPos, scroll.Position);
+
+        Application.Refresh ();
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (expectedOut, _output);
+
+        Application.OnMouseEvent (
+                                  new ()
+                                  {
+                                      Position = orientation == Orientation.Vertical ? new (0, startLocation) : new (startLocation, 0),
+                                      Flags = MouseFlags.Button1Released
+                                  });
+        Assert.Null (Application.MouseGrabView);
+    }
+
+    [Theory]
+    [InlineData (Orientation.Vertical, 20, 10)]
+    [InlineData (Orientation.Vertical, 40, 30)]
+    public void Position_Cannot_Be_Negative_Nor_Greater_Than_Size_Minus_Frame_Length (Orientation orientation, int size, int expectedPos)
+    {
+        var scroll = new Scroll { Orientation = orientation, Height = 10, Size = size };
+        Assert.Equal (0, scroll.Position);
+
+        scroll.Position = -1;
+        Assert.Equal (0, scroll.Position);
+
+        scroll.Position = size;
+        Assert.Equal (0, scroll.Position);
+
+        scroll.Position = expectedPos;
+        Assert.Equal (expectedPos, scroll.Position);
+    }
+
+    [Fact]
+    public void PositionChanging_Cancelable_And_PositionChanged_Events ()
+    {
+        var changingCount = 0;
+        var changedCount = 0;
+        var scroll = new Scroll { Size = 10 };
+
+        scroll.PositionChanging += (s, e) =>
+                                   {
+                                       if (changingCount == 0)
+                                       {
+                                           e.Cancel = true;
+                                       }
+
+                                       changingCount++;
+                                   };
+        scroll.PositionChanged += (s, e) => changedCount++;
+
+        scroll.Position = 1;
+        Assert.Equal (0, scroll.Position);
+        Assert.Equal (1, changingCount);
+        Assert.Equal (0, changedCount);
+
+        scroll.Position = 1;
+        Assert.Equal (1, scroll.Position);
+        Assert.Equal (2, changingCount);
+        Assert.Equal (1, changedCount);
+    }
+
+    [Fact]
+    public void SizeChanged_Event ()
+    {
+        var count = 0;
+        var scroll = new Scroll ();
+        scroll.SizeChanged += (s, e) => count++;
+
+        scroll.Size = 10;
+        Assert.Equal (10, scroll.Size);
+        Assert.Equal (1, count);
+    }
+
+    [Theory]
+    [AutoInitShutdown]
+    [InlineData (
+                    3,
+                    10,
+                    Orientation.Vertical,
+                    @"
+┌─┐
+│█│
+│█│
+│█│
+│░│
+│░│
+│░│
+│░│
+│░│
+└─┘")]
+    [InlineData (
+                    10,
+                    3,
+                    Orientation.Horizontal,
+                    @"
+┌────────┐
+│███░░░░░│
+└────────┘")]
+    public void Vertical_Horizontal_Draws_Correctly (int width, int height, Orientation orientation, string expected)
+    {
+        var super = new Window { Id = "super", Width = Dim.Fill (), Height = Dim.Fill () };
+        var top = new Toplevel ();
+        top.Add (super);
+
+        var scroll = new Scroll
+        {
+            Orientation = orientation,
+            Size = orientation == Orientation.Vertical ? height * 2 : width * 2,
+            Width = orientation == Orientation.Vertical ? 1 : Dim.Fill (),
+            Height = orientation == Orientation.Vertical ? Dim.Fill () : 1
+        };
+        super.Add (scroll);
+
+        Application.Begin (top);
+        ((FakeDriver)Application.Driver).SetBufferSize (width, height);
+
+        _ = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
+    }
+}