#nullable enable
using System.ComponentModel;
using System.Drawing;
namespace Terminal.Gui;
///
/// Indicates the size of scrollable content and provides a visible element, referred to as the "ScrollSlider" that
/// that is sized to
/// show the proportion of the scrollable content to the size of the . The ScrollSlider
/// can be dragged with the mouse. A Scroll can be oriented either vertically or horizontally and is used within a
/// .
///
///
///
/// By default, this view cannot be focused and does not support keyboard.
///
///
public class ScrollBar : View, IOrientation, IDesignable
{
private readonly Button _decreaseButton;
internal readonly ScrollSlider _slider;
private readonly Button _increaseButton;
///
public ScrollBar ()
{
_decreaseButton = new ()
{
CanFocus = false,
NoDecorations = true,
NoPadding = true,
ShadowStyle = ShadowStyle.None,
WantContinuousButtonPressed = true
};
_decreaseButton.Accepting += OnDecreaseButtonOnAccept;
_slider = new ()
{
SliderPadding = 2, // For the buttons
};
_slider.Scrolled += SliderOnScroll;
_slider.PositionChanged += SliderOnPositionChanged;
_increaseButton = new ()
{
CanFocus = false,
NoDecorations = true,
NoPadding = true,
ShadowStyle = ShadowStyle.None,
WantContinuousButtonPressed = true
};
_increaseButton.Accepting += OnIncreaseButtonOnAccept;
base.Add (_decreaseButton, _slider, _increaseButton);
CanFocus = false;
_orientationHelper = new (this); // Do not use object initializer!
_orientationHelper.Orientation = Orientation.Vertical;
//_orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
//_orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
// This sets the width/height etc...
OnOrientationChanged (Orientation);
void OnDecreaseButtonOnAccept (object? s, CommandEventArgs e)
{
Position -= Increment;
e.Cancel = true;
}
void OnIncreaseButtonOnAccept (object? s, CommandEventArgs e)
{
Position += Increment;
e.Cancel = true;
}
}
///
protected override void OnFrameChanged (in Rectangle frame)
{
if (Orientation == Orientation.Vertical)
{
_slider.VisibleContentSize = Viewport.Height;
}
else
{
_slider.VisibleContentSize = Viewport.Width;
}
_slider.Size = CalculateSliderSize ();
ShowHide ();
}
private void ShowHide ()
{
if (!AutoHide)
{
return;
}
if (Orientation == Orientation.Vertical)
{
Visible = Frame.Height < ScrollableContentSize;
}
else
{
Visible = Frame.Width < ScrollableContentSize;
}
_slider.Size = CalculateSliderSize ();
_sliderPosition = CalculateSliderPositionFromContentPosition (_position, NavigationDirection.Forward);
_slider.Position = _sliderPosition.Value;
}
///
protected override void OnSubviewLayout (LayoutEventArgs args)
{
}
private void PositionSubviews ()
{
if (Orientation == Orientation.Vertical)
{
_decreaseButton.Y = 0;
_decreaseButton.X = 0;
_decreaseButton.Width = Dim.Fill ();
_decreaseButton.Height = 1;
_decreaseButton.Title = Glyphs.UpArrow.ToString ();
_slider.X = 0;
_slider.Y = 1;
_slider.Width = Dim.Fill ();
_increaseButton.Y = Pos.AnchorEnd ();
_increaseButton.X = 0;
_increaseButton.Width = Dim.Fill ();
_increaseButton.Height = 1;
_increaseButton.Title = Glyphs.DownArrow.ToString ();
}
else
{
_decreaseButton.Y = 0;
_decreaseButton.X = 0;
_decreaseButton.Width = 1;
_decreaseButton.Height = Dim.Fill ();
_decreaseButton.Title = Glyphs.LeftArrow.ToString ();
_slider.Y = 0;
_slider.X = 1;
_slider.Height = Dim.Fill ();
_increaseButton.Y = 0;
_increaseButton.X = Pos.AnchorEnd ();
_increaseButton.Width = 1;
_increaseButton.Height = Dim.Fill ();
_increaseButton.Title = Glyphs.RightArrow.ToString ();
}
}
#region IOrientation members
private readonly OrientationHelper _orientationHelper;
///
public Orientation Orientation
{
get => _orientationHelper.Orientation;
set => _orientationHelper.Orientation = value;
}
///
public event EventHandler>? OrientationChanging;
///
public event EventHandler>? OrientationChanged;
///
public void OnOrientationChanged (Orientation newOrientation)
{
TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
TextAlignment = Alignment.Center;
VerticalTextAlignment = Alignment.Center;
X = 0;
Y = 0;
if (Orientation == Orientation.Vertical)
{
Width = 1;
Height = Dim.Fill ();
}
else
{
Width = Dim.Fill ();
Height = 1;
}
_slider.Orientation = newOrientation;
PositionSubviews ();
OrientationChanged?.Invoke (this, new (newOrientation));
}
#endregion
///
/// Gets or sets the amount each mouse wheel event will incremenet/decrement the .
///
///
/// The default is 1.
///
public int Increment { get; set; } = 1;
private bool _autoHide = true;
///
/// Gets or sets whether will be set to if the dimension of the
/// scroll bar is greater than or equal to .
///
public bool AutoHide
{
get => _autoHide;
set
{
if (_autoHide != value)
{
_autoHide = value;
if (!AutoHide)
{
Visible = true;
}
SetNeedsLayout ();
}
}
}
public bool KeepContentInAllViewport
{
//get => _scroll.KeepContentInAllViewport;
//set => _scroll.KeepContentInAllViewport = value;
get;
set;
}
///
/// Gets or sets whether the Scroll will show the percentage the slider
/// takes up within the .
///
public bool ShowPercent
{
get => _slider.ShowPercent;
set => _slider.ShowPercent = value;
}
private int? _visibleContentSize;
///
/// Gets or sets the size of the visible viewport into the content being scrolled, bounded by .
///
///
/// If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
///
public int VisibleContentSize
{
get
{
if (_visibleContentSize.HasValue)
{
return _visibleContentSize.Value;
}
return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
}
set
{
_visibleContentSize = value;
_slider.Size = CalculateSliderSize ();
}
}
private int _scrollableContentSize;
///
/// Gets or sets the size of the content that can be scrolled. This is typically set to .
///
public int ScrollableContentSize
{
get => _scrollableContentSize;
set
{
if (value == _scrollableContentSize || value < 0)
{
return;
}
_scrollableContentSize = value;
_slider.Size = CalculateSliderSize ();
OnSizeChanged (_scrollableContentSize);
ScrollableContentSizeChanged?.Invoke (this, new (in _scrollableContentSize));
SetNeedsLayout ();
}
}
/// Called when has changed.
protected virtual void OnSizeChanged (int size) { }
/// Raised when has changed.
public event EventHandler>? ScrollableContentSizeChanged;
#region Position
private int _position;
///
/// Gets or sets the position of the slider relative to .
///
///
///
/// The content position is clamped to 0 and minus .
///
///
/// Setting will result in the and
/// events being raised.
///
///
public int Position
{
get => _position;
set
{
if (value == _position)
{
return;
}
// Clamp the value between 0 and Size - VisibleContentSize
int newContentPosition = (int)Math.Clamp (value, 0, Math.Max (0, ScrollableContentSize - VisibleContentSize));
NavigationDirection direction = newContentPosition >= _position ? NavigationDirection.Forward : NavigationDirection.Backward;
if (OnPositionChanging (_position, newContentPosition))
{
return;
}
CancelEventArgs args = new (ref _position, ref newContentPosition);
PositionChanging?.Invoke (this, args);
if (args.Cancel)
{
return;
}
int distance = newContentPosition - _position;
_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));
}
}
///
/// Called when is changing. Return true to cancel the change.
///
protected virtual bool OnPositionChanging (int currentPos, int newPos) { return false; }
///
/// Raised when the is changing. Set to
/// to prevent the position from being changed.
///
public event EventHandler>? PositionChanging;
/// Called when has changed.
protected virtual void OnPositionChanged (int position) { }
/// Raised when the has changed.
public event EventHandler>? PositionChanged;
/// Called when has changed. Indicates how much to scroll.
protected virtual void OnScrolled (int distance) { }
/// Raised when the has changed. Indicates how much to scroll.
public event EventHandler>? Scrolled;
///
/// INTERNAL API (for unit tests) - Calculates the position within the based on the slider position.
///
///
/// Clamps the sliderPosition, ensuring the returned content position is always less than
/// - .
///
///
///
internal int CalculatePositionFromSliderPosition (int sliderPosition)
{
int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
return ScrollSlider.CalculateContentPosition (ScrollableContentSize, VisibleContentSize, sliderPosition, scrollBarSize - _slider.SliderPadding);
}
#endregion ContentPosition
#region Slider Management
private int? _sliderPosition;
///
/// INTERNAL (for unit tests). Calculates the size of the slider based on the Orientation, VisibleContentSize, the actual Viewport, and Size.
///
///
internal int CalculateSliderSize ()
{
int maxSliderSize = (Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width) - 2;
return ScrollSlider.CalculateSize (ScrollableContentSize, VisibleContentSize, maxSliderSize);
}
private void SliderOnPositionChanged (object? sender, EventArgs e)
{
if (VisibleContentSize == 0)
{
return;
}
RaiseSliderPositionChangeEvents (_sliderPosition, e.CurrentValue);
}
private void SliderOnScroll (object? sender, EventArgs e)
{
if (VisibleContentSize == 0)
{
return;
}
int calculatedSliderPos = CalculateSliderPositionFromContentPosition (_position, e.CurrentValue >= 0 ? NavigationDirection.Forward : NavigationDirection.Backward);
if (calculatedSliderPos == _sliderPosition)
{
return;
}
int sliderScrolledAmount = e.CurrentValue;
int calculatedPosition = CalculatePositionFromSliderPosition (calculatedSliderPos + sliderScrolledAmount);
Position = calculatedPosition;
}
///
/// Gets or sets the position of the start of the Scroll slider, within the Viewport.
///
public int GetSliderPosition () => CalculateSliderPositionFromContentPosition (_position);
private void RaiseSliderPositionChangeEvents (int? currentSliderPosition, int newSliderPosition)
{
if (currentSliderPosition == newSliderPosition)
{
return;
}
_sliderPosition = newSliderPosition;
OnSliderPositionChanged (newSliderPosition);
SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
}
/// Called when the slider position has changed.
protected virtual void OnSliderPositionChanged (int position) { }
/// Raised when the slider position has changed.
public event EventHandler>? SliderPositionChanged;
///
/// INTERNAL API (for unit tests) - Calculates the position of the slider based on the content position.
///
///
///
///
internal int CalculateSliderPositionFromContentPosition (int contentPosition, NavigationDirection direction = NavigationDirection.Forward)
{
int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
return ScrollSlider.CalculatePosition (ScrollableContentSize, VisibleContentSize, contentPosition, scrollBarSize - 2, direction);
}
#endregion Slider Management
///
protected override bool OnClearingViewport ()
{
if (Orientation == Orientation.Vertical)
{
FillRect (Viewport with { Y = Viewport.Y + 1, Height = Viewport.Height - 2 }, Glyphs.Stipple);
}
else
{
FillRect (Viewport with { X = Viewport.X + 1, Width = Viewport.Width - 2 }, Glyphs.Stipple);
}
SetNeedsDraw ();
return true;
}
// TODO: Change this to work OnMouseEvent with continuouse press and grab so it's continous.
///
protected override bool OnMouseClick (MouseEventArgs args)
{
// Check if the mouse click is a single click
if (!args.IsSingleClicked)
{
return false;
}
int sliderCenter;
int distanceFromCenter;
if (Orientation == Orientation.Vertical)
{
sliderCenter = 1 + _slider.Frame.Y + _slider.Frame.Height / 2;
distanceFromCenter = args.Position.Y - sliderCenter;
}
else
{
sliderCenter = 1 + _slider.Frame.X + _slider.Frame.Width / 2;
distanceFromCenter = args.Position.X - sliderCenter;
}
#if PROPORTIONAL_SCROLL_JUMP
// BUGBUG: This logic mostly works to provide a proportional jump. However, the math
// BUGBUG: falls apart in edge cases. Most other scroll bars (e.g. Windows) do not do proportional
// BUGBUG: Thus, this is disabled and we just jump a page each click.
// Ratio of the distance to the viewport dimension
double ratio = (double)Math.Abs (distanceFromCenter) / (VisibleContentSize);
// Jump size based on the ratio and the total content size
int jump = (int)(ratio * (Size - VisibleContentSize));
#else
int jump = (VisibleContentSize);
#endif
// Adjust the content position based on the distance
if (distanceFromCenter < 0)
{
Position = Math.Max (0, Position - jump);
}
else
{
Position = Math.Min (ScrollableContentSize - _slider.VisibleContentSize, Position + jump);
}
return true;
}
///
protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
{
if (SuperView is null)
{
return false;
}
if (!mouseEvent.IsWheel)
{
return false;
}
if (Orientation == Orientation.Vertical)
{
if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown))
{
Position += Increment;
}
if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp))
{
Position -= Increment;
}
}
else
{
if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight))
{
Position += Increment;
}
if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft))
{
Position -= Increment;
}
}
return true;
}
///
public bool EnableForDesign ()
{
OrientationChanged += (sender, args) =>
{
if (args.CurrentValue == Orientation.Vertical)
{
Width = 1;
Height = Dim.Fill ();
}
else
{
Width = Dim.Fill ();
Height = 1;
}
};
Width = 1;
Height = Dim.Fill ();
ScrollableContentSize = 250;
return true;
}
}