Browse Source

We're back!

Tig 8 months ago
parent
commit
ecfa51cf93

+ 34 - 68
Terminal.Gui/Views/ScrollBar/ScrollBar.cs

@@ -86,9 +86,10 @@ public class ScrollBar : View, IOrientation, IDesignable
         }
         else
         {
-            _slider.VisibleContentSize = Viewport.Width ;
+            _slider.VisibleContentSize = Viewport.Width;
         }
 
+        _slider.Size = CalculateSliderSize ();
         ShowHide ();
     }
 
@@ -109,7 +110,8 @@ public class ScrollBar : View, IOrientation, IDesignable
         }
 
         _slider.Size = CalculateSliderSize ();
-        _slider.Position = CalculateSliderPosition (_position, NavigationDirection.Forward);
+        _sliderPosition = CalculateSliderPositionFromContentPosition (_position, NavigationDirection.Forward);
+        _slider.Position = _sliderPosition.Value;
     }
 
     /// <inheritdoc/>
@@ -210,7 +212,7 @@ public class ScrollBar : View, IOrientation, IDesignable
     ///     The default is 1.
     /// </remarks>
     public int Increment { get; set; } = 1;
-    
+
     private bool _autoHide = true;
 
     /// <summary>
@@ -356,25 +358,18 @@ public class ScrollBar : View, IOrientation, IDesignable
 
             _position = newContentPosition;
 
+            _sliderPosition = CalculateSliderPositionFromContentPosition (_position, direction);
+
+            if (_slider.Position != _sliderPosition)
+            {
+                _slider.Position = _sliderPosition.Value;
+            }
+
             OnPositionChanged (_position);
             PositionChanged?.Invoke (this, new (in _position));
 
             OnScrolled (distance);
             Scrolled?.Invoke (this, new (in distance));
-
-            int currentSliderPosition = _slider.Position;
-
-            //_slider.Size = CalculateSliderSize ();
-
-            //int calculatedSliderPosition = CalculateSliderPosition (_position, direction);
-
-            //_slider.MoveToPosition (calculatedSliderPosition);
-
-            _slider.Size = CalculateSliderSize ();
-            int calculatedSliderPosition = CalculateSliderPosition (_position, direction);
-
-            RaiseSliderPositionChangeEvents (calculatedSliderPosition, currentSliderPosition);
-
         }
     }
 
@@ -411,29 +406,19 @@ public class ScrollBar : View, IOrientation, IDesignable
     /// </remarks>
     /// <param name="sliderPosition"></param>
     /// <returns></returns>
-    internal int CalculatePosition (int sliderPosition)
+    internal int CalculatePositionFromSliderPosition (int sliderPosition)
     {
-        // Clamp the slider 
-        int clampedSliderPosition = _slider.ClampPosition (sliderPosition);
         int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
-        double pos = (double)(clampedSliderPosition) /
-                     (scrollBarSize - _slider.Size - _slider.SliderPadding) * (ScrollableContentSize - VisibleContentSize);
-
-        if (pos is double.NaN)
-        {
-            return 0;
-        }
-        double rounded = Math.Ceiling (pos);
-
-        // Clamp between 0 and Size - SliderSize
-        return (int)Math.Clamp (rounded, 0, Math.Max (0, ScrollableContentSize - _slider.Size));
+        return ScrollSlider.CalculateContentPosition (ScrollableContentSize, VisibleContentSize, sliderPosition, scrollBarSize - _slider.SliderPadding);
     }
 
     #endregion ContentPosition
 
 
     #region Slider Management
-    
+
+    private int? _sliderPosition;
+
     /// <summary>
     ///     INTERNAL (for unit tests). Calculates the size of the slider based on the Orientation, VisibleContentSize, the actual Viewport, and Size.
     /// </summary>
@@ -441,19 +426,7 @@ public class ScrollBar : View, IOrientation, IDesignable
     internal int CalculateSliderSize ()
     {
         int maxSliderSize = (Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width) - 2;
-
-        if (maxSliderSize < 1 || VisibleContentSize == 0)
-        {
-            return 1;
-        }
-
-        //if (ScrollableContentSize < VisibleContentSize)
-        //{
-        //    return maxSliderSize ;
-        //}
-        
-        int size = (int)Math.Clamp (Math.Floor ((double)VisibleContentSize / ScrollableContentSize * (maxSliderSize)), 1, VisibleContentSize);
-        return Math.Clamp (size, 1, maxSliderSize);
+        return ScrollSlider.CalculateSize (ScrollableContentSize, VisibleContentSize, maxSliderSize);
     }
 
     private void SliderOnPositionChanged (object? sender, EventArgs<int> e)
@@ -463,9 +436,7 @@ public class ScrollBar : View, IOrientation, IDesignable
             return;
         }
 
-        int pos = e.CurrentValue;
-
-        RaiseSliderPositionChangeEvents (_slider.Position, pos);
+        RaiseSliderPositionChangeEvents (_sliderPosition, e.CurrentValue);
     }
 
     private void SliderOnScroll (object? sender, EventArgs<int> e)
@@ -475,29 +446,31 @@ public class ScrollBar : View, IOrientation, IDesignable
             return;
         }
 
-        int calculatedSliderPos = CalculateSliderPosition (_position, e.CurrentValue >= 0 ? NavigationDirection.Forward : NavigationDirection.Backward);
-        int sliderScrolledAmount = e.CurrentValue;
-        int scrolledAmount = CalculatePosition (sliderScrolledAmount);
+        int calculatedSliderPos = CalculateSliderPositionFromContentPosition (_position, e.CurrentValue >= 0 ? NavigationDirection.Forward : NavigationDirection.Backward);
 
-        RaiseSliderPositionChangeEvents (calculatedSliderPos, _slider.Position);
+        if (calculatedSliderPos == _sliderPosition)
+        {
+            return;
+        }
+        int sliderScrolledAmount = e.CurrentValue;
+        int calculatedPosition = CalculatePositionFromSliderPosition (calculatedSliderPos + sliderScrolledAmount);
 
-        Position = _position + scrolledAmount;
+        Position = calculatedPosition;
     }
 
     /// <summary>
     ///     Gets or sets the position of the start of the Scroll slider, within the Viewport.
     /// </summary>
-    public int GetSliderPosition () => CalculateSliderPosition (_position);
+    public int GetSliderPosition () => CalculateSliderPositionFromContentPosition (_position);
 
-    private void RaiseSliderPositionChangeEvents (int calculatedSliderPosition, int newSliderPosition)
+    private void RaiseSliderPositionChangeEvents (int? currentSliderPosition, int newSliderPosition)
     {
-        if (calculatedSliderPosition == newSliderPosition)
+        if (currentSliderPosition == newSliderPosition)
         {
             return;
         }
 
-        // This sets the slider position and clamps the value
-        _slider.Position = newSliderPosition;
+        _sliderPosition = newSliderPosition;
 
         OnSliderPositionChanged (newSliderPosition);
         SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
@@ -515,17 +488,10 @@ public class ScrollBar : View, IOrientation, IDesignable
     /// <param name="contentPosition"></param>
     /// <param name="direction"></param>
     /// <returns></returns>
-    internal int CalculateSliderPosition (int contentPosition, NavigationDirection direction = NavigationDirection.Forward)
+    internal int CalculateSliderPositionFromContentPosition (int contentPosition, NavigationDirection direction = NavigationDirection.Forward)
     {
         int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
-        if (scrollBarSize < 3 || ScrollableContentSize - VisibleContentSize == 0)
-        {
-            return 0;
-        }
-
-        double newSliderPosition = (double)(contentPosition - 1) / (ScrollableContentSize - VisibleContentSize) * (scrollBarSize - _slider.Size - _slider.SliderPadding);
-
-        return Math.Clamp (direction == NavigationDirection.Forward ? (int)Math.Floor (newSliderPosition) : (int)Math.Ceiling (newSliderPosition), 0, scrollBarSize - _slider.Size - _slider.SliderPadding);
+        return ScrollSlider.CalculatePosition (ScrollableContentSize, VisibleContentSize, contentPosition, scrollBarSize - 2, direction);
     }
 
 
@@ -542,7 +508,7 @@ public class ScrollBar : View, IOrientation, IDesignable
         {
             FillRect (Viewport with { X = Viewport.X + 1, Width = Viewport.Width - 2 }, Glyphs.Stipple);
         }
-
+        SetNeedsDraw ();
         return true;
     }
 

+ 90 - 2
Terminal.Gui/Views/ScrollBar/ScrollSlider.cs

@@ -1,6 +1,8 @@
 #nullable enable
 
 using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
 
 namespace Terminal.Gui;
 
@@ -254,7 +256,7 @@ public class ScrollSlider : View, IOrientation, IDesignable
     /// <returns></returns>
     internal int ClampPosition (int newPosition)
     {
-        return Math.Clamp (newPosition, 0, Math.Max (0, VisibleContentSize - SliderPadding - Size));
+        return Math.Clamp (newPosition, 0, Math.Max (SliderPadding / 2, VisibleContentSize - SliderPadding - Size));
     }
 
     private void RaisePositionChangeEvents (int newPosition)
@@ -390,6 +392,8 @@ public class ScrollSlider : View, IOrientation, IDesignable
                     currentLocation = Frame.X;
                 }
 
+                currentLocation -= SliderPadding / 2;
+
                 // location does not account for the ShrinkBy
                 int sliderLowerBound = SliderPadding / 2;
                 int sliderUpperBound = superViewDimension - SliderPadding / 2 - Size;
@@ -429,10 +433,94 @@ public class ScrollSlider : View, IOrientation, IDesignable
     /// <inheritdoc/>
     public bool EnableForDesign ()
     {
-//        Orientation = Orientation.Horizontal;
+        //        Orientation = Orientation.Horizontal;
         ShowPercent = true;
         Size = 5;
 
         return true;
     }
+
+    /// <summary>
+    ///     Gets the slider size.
+    /// </summary>
+    /// <param name="scrollableContentSize">The size of the content.</param>
+    /// <param name="visibleContentSize">The size of the visible content.</param>
+    /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
+    public static int CalculateSize (
+        int scrollableContentSize,
+        int visibleContentSize,
+        int sliderBounds
+    )
+    {
+        if (scrollableContentSize <= 0 || sliderBounds <= 0)
+        {
+            return 1;   // Slider must be at least 1
+        }
+
+        if (visibleContentSize <= 0 || scrollableContentSize <= visibleContentSize)
+        {
+            return sliderBounds;
+        }
+
+        double sliderSizeD = ((double)visibleContentSize / scrollableContentSize) * sliderBounds;
+
+        int sliderSize = (int)Math.Floor (sliderSizeD);
+
+        return Math.Clamp (sliderSize, 1, sliderBounds);
+    }
+
+    /// <summary>
+    ///     Gets the slider position.
+    /// </summary>
+    /// <param name="scrollableContentSize">The size of the content.</param>
+    /// <param name="visibleContentSize">The size of the visible content.</param>
+    /// <param name="contentPosition">The position in the content (between 0 and <paramref name="scrollableContentSize"/>).</param>
+    /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
+    /// <param name="direction">The direction the slider is moving.</param>
+    public static int CalculatePosition (
+        int scrollableContentSize,
+        int visibleContentSize,
+        int contentPosition,
+        int sliderBounds,
+        NavigationDirection direction
+    )
+    {
+        if (scrollableContentSize - visibleContentSize <= 0 || sliderBounds <= 0)
+        {
+            return 0;
+        }
+
+        int calculatedSliderSize = CalculateSize (scrollableContentSize, visibleContentSize, sliderBounds);
+
+        double newSliderPosition = ((double)contentPosition / (scrollableContentSize - visibleContentSize)) * (sliderBounds - calculatedSliderSize);
+
+        return Math.Clamp ((int)Math.Round (newSliderPosition), 0, sliderBounds - calculatedSliderSize);
+    }
+
+    /// <summary>
+    ///     Gets the content position.
+    /// </summary>
+    /// <param name="scrollableContentSize">The size of the content.</param>
+    /// <param name="visibleContentSize">The size of the visible content.</param>
+    /// <param name="sliderPosition">The position of the slider.</param>
+    /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
+    public static int CalculateContentPosition (
+        int scrollableContentSize,
+        int visibleContentSize,
+        int sliderPosition,
+        int sliderBounds
+    )
+    {
+        int sliderSize = CalculateSize (scrollableContentSize, visibleContentSize, sliderBounds);
+
+        double pos = ((double)(sliderPosition) / (sliderBounds - sliderSize)) * (scrollableContentSize - visibleContentSize);
+
+        if (pos is double.NaN)
+        {
+            return 0;
+        }
+        double rounded = Math.Ceiling (pos);
+
+        return (int)Math.Clamp (rounded, 0, Math.Max (0, scrollableContentSize - sliderSize));
+    }
 }

+ 11 - 1
UICatalog/Scenarios/CharacterMap/CharacterMap.cs

@@ -66,7 +66,17 @@ public class CharacterMap : Scenario
         };
         top.Add (jumpEdit);
 
-        _charMap.SelectedCodePointChanged += (sender, args) => jumpEdit.Text = ((Rune)args.CurrentValue).ToString ();
+        _charMap.SelectedCodePointChanged += (sender, args) =>
+                                             {
+                                                 if (Rune.IsValid (args.CurrentValue))
+                                                 {
+                                                     jumpEdit.Text = ((Rune)args.CurrentValue).ToString ();
+                                                 }
+                                                 else
+                                                 {
+                                                     jumpEdit.Text = $"U+{args.CurrentValue:x5}";
+                                                 }
+                                             };
 
         _errorLabel = new ()
         {

+ 3 - 3
UnitTests/Views/ScrollBarTests.cs

@@ -473,7 +473,7 @@ public class ScrollBarTests (ITestOutputHelper output)
         scrollBar.Width = visibleContentSize;
 
         // Act
-        var sliderPosition= scrollBar.CalculateSliderPosition (contentPosition, NavigationDirection.Forward);
+        var sliderPosition= scrollBar.CalculateSliderPositionFromContentPosition (contentPosition, NavigationDirection.Forward);
 
         // Assert
         Assert.Equal (expectedSliderPosition, sliderPosition);
@@ -575,7 +575,7 @@ public class ScrollBarTests (ITestOutputHelper output)
         scrollBar.Frame = new (0, 0, visibleContentSize, 0);
 
         // Act
-        var contentPosition = scrollBar.CalculatePosition (sliderPosition);
+        var contentPosition = scrollBar.CalculatePositionFromSliderPosition (sliderPosition);
 
         // Assert
         Assert.Equal (expectedContentPosition, contentPosition);
@@ -1150,7 +1150,7 @@ public class ScrollBarTests (ITestOutputHelper output)
         scrollBar.ScrollableContentSize = contentSize;
         scrollBar.Position = contentPosition;
 
-        int sliderPos = scrollBar.CalculateSliderPosition (contentPosition, NavigationDirection.Forward);
+        int sliderPos = scrollBar.CalculateSliderPositionFromContentPosition (contentPosition, NavigationDirection.Forward);
 
         super.BeginInit ();
         super.EndInit ();

+ 333 - 2
UnitTests/Views/ScrollSliderTests.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics;
+using System.Runtime.InteropServices;
 using Microsoft.VisualStudio.TestPlatform.Utilities;
 using Xunit.Abstractions;
 using static Unix.Terminal.Delegates;
@@ -131,6 +132,73 @@ public class ScrollSliderTests (ITestOutputHelper output)
         Assert.True (scrollSlider.Size <= dimension);
     }
 
+
+    [Theory]
+    [CombinatorialData]
+
+    public void CalculateSize_ScrollBounds_0_Returns_1 ([CombinatorialRange (-1, 5, 1)] int visibleContentSize, [CombinatorialRange (-1, 5, 1)] int scrollableContentSize)
+    {
+        // Arrange
+
+        // Act
+        var sliderSize = ScrollSlider.CalculateSize (scrollableContentSize, visibleContentSize, 0);
+
+        // Assert
+        Assert.Equal (1, sliderSize);
+    }
+
+    [Theory]
+    [CombinatorialData]
+
+    public void CalculateSize_ScrollableContentSize_0_Returns_1 ([CombinatorialRange (-1, 5, 1)] int visibleContentSize, [CombinatorialRange (-1, 5, 1)] int sliderBounds)
+    {
+        // Arrange
+
+        // Act
+        var sliderSize = ScrollSlider.CalculateSize (0, visibleContentSize, sliderBounds);
+
+        // Assert
+        Assert.Equal (1, sliderSize);
+    }
+
+
+    //[Theory]
+    //[CombinatorialData]
+
+    //public void CalculateSize_VisibleContentSize_0_Returns_0 ([CombinatorialRange (-1, 5, 1)] int scrollableContentSize, [CombinatorialRange (-1, 5, 1)] int sliderBounds)
+    //{
+    //    // Arrange
+
+    //    // Act
+    //    var sliderSize = ScrollSlider.CalculateSize (scrollableContentSize, 0, sliderBounds);
+
+    //    // Assert
+    //    Assert.Equal (0, sliderSize);
+    //}
+
+
+    [Theory]
+    [InlineData (0, 1, 1, 1)]
+    [InlineData (1, 1, 1, 1)]
+    [InlineData (1, 2, 1, 1)]
+    [InlineData (0, 5, 5, 5)]
+    [InlineData (1, 5, 5, 1)]
+    [InlineData (2, 5, 5, 2)]
+    [InlineData (3, 5, 5, 3)]
+    [InlineData (4, 5, 5, 4)]
+    [InlineData (5, 5, 5, 5)]
+    [InlineData (6, 5, 5, 5)]
+    public void CalculateSize_Calculates_Correctly (int visibleContentSize, int scrollableContentSize, int scrollBounds, int expectedSliderSize)
+    {
+        // Arrange
+
+        // Act
+        var sliderSize = ScrollSlider.CalculateSize (scrollableContentSize, visibleContentSize, scrollBounds);
+
+        // Assert
+        Assert.Equal (expectedSliderSize, sliderSize);
+    }
+
     [Fact]
     public void VisibleContentSize_Not_Set_Uses_SuperView ()
     {
@@ -210,6 +278,269 @@ public class ScrollSliderTests (ITestOutputHelper output)
         Assert.InRange (scrollSlider.VisibleContentSize, 1, 10);
     }
 
+    [Theory]
+    //// 0123456789
+    ////  ---------
+    //// ◄█►
+    //[InlineData (3, 3, 0, 1, 0)]
+    //[InlineData (3, 3, 1, 1, 0)]
+    //[InlineData (3, 3, 2, 1, 0)]
+
+    //// 0123456789
+    ////  ---------
+    //// ◄██►
+    //[InlineData (4, 4, 0, 2, 0)]
+    //[InlineData (4, 4, 1, 2, 0)]
+    //[InlineData (4, 4, 2, 2, 0)]
+    //[InlineData (4, 4, 3, 2, 0)]
+    //[InlineData (4, 4, 4, 2, 0)]
+
+    // 012345
+    // ^----
+    // █░
+    [InlineData (2, 5, 0, 0)]
+    // -^---
+    // █░
+    [InlineData (2, 5, 1, 0)]
+    // --^--
+    // █░
+    [InlineData (2, 5, 2, 0)]
+    // ---^-
+    // ░█
+    [InlineData (2, 5, 3, 1)]
+    // ----^
+    // ░█
+    [InlineData (2, 5, 4, 1)]
+
+    // 012345
+    // ^----
+    // █░░
+    [InlineData (3, 5, 0, 0)]
+    // -^---
+    // ░█░
+    [InlineData (3, 5, 1, 1)]
+    // --^--
+    // ░░█
+    [InlineData (3, 5, 2, 2)]
+    // ---^-
+    // ░░█
+    [InlineData (3, 5, 3, 2)]
+    // ----^
+    // ░░█
+    [InlineData (3, 5, 4, 2)]
+
+
+    // 0123456789
+    // ^-----
+    // █░░
+    [InlineData (3, 6, 0, 0)]
+    // -^----
+    // █░░
+    [InlineData (3, 6, 1, 0)]
+    // --^---
+    // ░█░
+    [InlineData (3, 6, 2, 1)]
+    // ---^--
+    // ░░█
+    [InlineData (3, 6, 3, 2)]
+    // ----^-
+    // ░░█
+    [InlineData (3, 6, 4, 2)]
+
+    // -----^
+    // ░░█
+    [InlineData (3, 6, 5, 2)]
+
+    // 012345
+    // ^----
+    // ███░
+    [InlineData (4, 5, 0, 0)]
+    // -^---
+    // ░███
+    [InlineData (4, 5, 1, 1)]
+    // --^--
+    // ░███
+    [InlineData (4, 5, 2, 1)]
+    // ---^-
+    // ░███
+    [InlineData (4, 5, 3, 1)]
+    // ----^
+    // ░███
+    [InlineData (4, 5, 4, 1)]
+
+    //// 01234
+    //// ^---------
+    //// ◄█░░►
+    //[InlineData (5, 10, 0, 1, 0)]
+    //// -^--------
+    //// ◄█░░►
+    //[InlineData (5, 10, 1, 1, 0)]
+    //// --^-------
+    //// ◄█░░►
+    //[InlineData (5, 10, 2, 1, 0)]
+    //// ---^------
+    //// ◄█░░►
+    //[InlineData (5, 10, 3, 1, 0)]
+    //// ----^----
+    //// ◄░█░►
+    //[InlineData (5, 10, 4, 1, 1)]
+    //// -----^---
+    //// ◄░█░►
+    //[InlineData (5, 10, 5, 1, 1)]
+    //// ------^--
+    //// ◄░░█►
+    //[InlineData (5, 10, 6, 1, 2)]
+    //// ------^--
+    //// ◄░░█►
+    //[InlineData (5, 10, 7, 1, 2)]
+    //// -------^-
+    //// ◄░░█►
+    //[InlineData (5, 10, 8, 1, 2)]
+    //// --------^
+    //// ◄░░█►
+    //[InlineData (5, 10, 9, 1, 2)]
+
+    // 0123456789
+    // ████░░░░
+    // ^-----------------
+    // 012345678901234567890123456789
+    // ░████░░░
+    // ----^-------------
+    // 012345678901234567890123456789
+    // ░░████░░
+    // --------^---------
+    // 012345678901234567890123456789
+    // ░░░████░
+    // ------------^-----
+    // 012345678901234567890123456789
+    // ░░░░████
+    // ----------------^--
+
+
+
+    // 0123456789
+    // ███░░░░░
+    // ^-----------------
+
+    // 012345678901234567890123456789
+    // ░░███░░░
+    // --------^---------
+    // 012345678901234567890123456789
+    // ░░░███░░
+    // ------------^-----
+    // 012345678901234567890123456789
+    // ░░░░███░
+    // ----------------^--
+    // 012345678901234567890123456789
+    // ░░░░░███
+    // ----------------^--
+
+
+    [InlineData (8, 18, 0, 0)]
+    [InlineData (8, 18, 1, 0)]
+    // 012345678901234567890123456789
+    // ░███░░░░
+    // --^---------------
+    [InlineData (8, 18, 2, 1)]
+    [InlineData (8, 18, 3, 1)]
+    [InlineData (8, 18, 4, 2)]
+    [InlineData (8, 18, 5, 2)]
+    [InlineData (8, 18, 6, 3)]
+    [InlineData (8, 18, 7, 3)]
+    [InlineData (8, 18, 8, 4)]
+    [InlineData (8, 18, 9, 4)]
+
+    // 012345678901234567890123456789
+    // ░░░░░███
+    // ----------^--------
+    [InlineData (8, 18, 10, 5)]
+    [InlineData (8, 18, 11, 5)]
+    [InlineData (8, 18, 12, 5)]
+    [InlineData (8, 18, 13, 5)]
+    [InlineData (8, 18, 14, 4)]
+    [InlineData (8, 18, 15, 5)]
+    [InlineData (8, 18, 16, 5)]
+    [InlineData (8, 18, 17, 5)]
+    [InlineData (8, 18, 18, 5)]
+    [InlineData (8, 18, 19, 5)]
+    [InlineData (8, 18, 20, 5)]
+    [InlineData (8, 18, 21, 5)]
+    [InlineData (8, 18, 22, 5)]
+    [InlineData (8, 18, 23, 5)]
+    // ------------------   ^
+    [InlineData (8, 18, 24, 5)]
+    [InlineData (8, 18, 25, 5)]
+
+    //// 0123456789
+    //// ◄████░░░░►
+    //// ^-----------------
+    //[InlineData (10, 20, 0, 5, 0)]
+    //[InlineData (10, 20, 1, 5, 0)]
+    //[InlineData (10, 20, 2, 5, 0)]
+    //[InlineData (10, 20, 3, 5, 0)]
+    //[InlineData (10, 20, 4, 5, 1)]
+    //[InlineData (10, 20, 5, 5, 1)]
+    //[InlineData (10, 20, 6, 5, 1)]
+    //[InlineData (10, 20, 7, 5, 2)]
+    //[InlineData (10, 20, 8, 5, 2)]
+    //[InlineData (10, 20, 9, 5, 2)]
+    //[InlineData (10, 20, 10, 5, 3)]
+    //[InlineData (10, 20, 11, 5, 3)]
+    //[InlineData (10, 20, 12, 5, 3)]
+    //[InlineData (10, 20, 13, 5, 3)]
+    //[InlineData (10, 20, 14, 5, 4)]
+    //[InlineData (10, 20, 15, 5, 4)]
+    //[InlineData (10, 20, 16, 5, 4)]
+    //[InlineData (10, 20, 17, 5, 5)]
+    //[InlineData (10, 20, 18, 5, 5)]
+    //[InlineData (10, 20, 19, 5, 5)]
+    //[InlineData (10, 20, 20, 5, 6)]
+    //[InlineData (10, 20, 21, 5, 6)]
+    //[InlineData (10, 20, 22, 5, 6)]
+    //[InlineData (10, 20, 23, 5, 6)]
+    //[InlineData (10, 20, 24, 5, 6)]
+    //[InlineData (10, 20, 25, 5, 6)]
+
+    public void CalculatePosition_Calculates_Correctly (int visibleContentSize, int scrollableContentSize, int contentPosition, int expectedSliderPosition)
+    {
+        // Arrange
+
+        // Act
+        var sliderPosition = ScrollSlider.CalculatePosition (
+                                                                 scrollableContentSize: scrollableContentSize,
+                                                                 visibleContentSize: visibleContentSize,
+                                                                 contentPosition: contentPosition,
+                                                                 sliderBounds: visibleContentSize,
+                                                                 NavigationDirection.Forward);
+
+        // Assert
+        Assert.Equal (expectedSliderPosition, sliderPosition);
+    }
+
+    [Theory]
+    [InlineData (8, 18, 0, 0)]
+
+    public void CalculateContentPosition_Calculates_Correctly (
+        int visibleContentSize,
+        int scrollableContentSize,
+        int sliderPosition,
+        int expectedContentPosition
+    )
+    {
+        // Arrange
+
+        // Act
+        var contentPosition = ScrollSlider.CalculateContentPosition (
+                                                             scrollableContentSize: scrollableContentSize,
+                                                             visibleContentSize: visibleContentSize,
+                                                             sliderPosition: sliderPosition,
+                                                             sliderBounds: visibleContentSize);
+
+        // Assert
+        Assert.Equal (expectedContentPosition, contentPosition);
+    }
+
+
     [Theory]
     [CombinatorialData]
     public void ClampPosition_WithSuperView_Clamps_To_ViewPort_Minus_Size_If_VisibleContentSize_Not_Set ([CombinatorialRange (10, 10, 1)] int dimension, [CombinatorialRange (1, 5, 1)] int sliderSize, [CombinatorialRange (-1, 10, 2)] int sliderPosition, Orientation orientation)
@@ -228,7 +559,7 @@ public class ScrollSliderTests (ITestOutputHelper output)
         super.Add (scrollSlider);
         super.Layout ();
 
-        Assert.Equal(dimension, scrollSlider.VisibleContentSize);
+        Assert.Equal (dimension, scrollSlider.VisibleContentSize);
 
         int clampedPosition = scrollSlider.ClampPosition (sliderPosition);
 
@@ -277,7 +608,7 @@ public class ScrollSliderTests (ITestOutputHelper output)
 
     [Theory]
     [CombinatorialData]
-    public void Position_Clamps_To_VisibleContentSize ([CombinatorialRange (0, 5, 1)] int dimension, [CombinatorialRange(1, 5, 1)] int sliderSize, [CombinatorialRange (-1, 10, 2)] int sliderPosition, Orientation orientation)
+    public void Position_Clamps_To_VisibleContentSize ([CombinatorialRange (0, 5, 1)] int dimension, [CombinatorialRange (1, 5, 1)] int sliderSize, [CombinatorialRange (-1, 10, 2)] int sliderPosition, Orientation orientation)
     {
         var scrollSlider = new ScrollSlider
         {