#nullable enable
using System.ComponentModel;
namespace Terminal.Gui;
///
/// The ScrollSlider can be dragged with the mouse, constrained by the size of the Viewport of it's superview. The
/// ScrollSlider can be
/// oriented either vertically or horizontally.
///
///
///
/// Used to represent the proportion of the visible content to the Viewport in a .
///
///
public class ScrollSlider : View, IOrientation, IDesignable
{
///
/// Initializes a new instance.
///
public ScrollSlider ()
{
Id = "scrollSlider";
WantMousePositionReports = true;
_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);
OnOrientationChanged (Orientation);
HighlightStyle = HighlightStyle.Hover;
}
#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;
// Reset Position to 0 when changing orientation
X = 0;
Y = 0;
Position = 0;
// Reset opposite dim to Dim.Fill ()
if (Orientation == Orientation.Vertical)
{
Height = Width;
Width = Dim.Fill ();
}
else
{
Width = Height;
Height = Dim.Fill ();
}
SetNeedsLayout ();
}
#endregion
///
protected override bool OnClearingViewport ()
{
if (Orientation == Orientation.Vertical)
{
FillRect (Viewport with { Height = Size }, Glyphs.ContinuousMeterSegment);
}
else
{
FillRect (Viewport with { Width = Size }, Glyphs.ContinuousMeterSegment);
}
return true;
}
private int? _size;
///
/// Gets or sets the size of the ScrollSlider. This is a helper that gets or sets Width or Height depending
/// on . The size will be clamped between 1 and the dimension of
/// the 's Viewport.
///
///
///
/// The dimension of the ScrollSlider that is perpendicular to the will be set to
///
///
///
public int Size
{
get => _size ?? 1;
set
{
if (value == _size)
{
return;
}
_size = Math.Clamp (value, 1, VisibleContentSize);
if (Orientation == Orientation.Vertical)
{
Height = _size;
}
else
{
Width = _size;
}
SetNeedsLayout ();
}
}
private int? _visibleContentSize;
///
/// Gets or sets the size of the viewport into the content being scrolled. If not explicitly set, will be the
/// greater of 1 and the dimension of the .
///
public int VisibleContentSize
{
get
{
if (_visibleContentSize.HasValue)
{
return _visibleContentSize.Value;
}
return Math.Max (1, Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 2048 : SuperView?.Viewport.Width ?? 2048);
}
set
{
if (value == _visibleContentSize)
{
return;
}
_visibleContentSize = int.Max (1, value);
if (_position >= _visibleContentSize - _size)
{
Position = _position;
}
SetNeedsLayout ();
}
}
private int _position;
///
/// 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 .
///
public int Position
{
get => _position;
set
{
int clampedPosition = ClampPosition (value);
if (_position == clampedPosition)
{
return;
}
RaisePositionChangeEvents (clampedPosition);
SetNeedsLayout ();
}
}
///
/// Moves the scroll slider to the specified position. Does not clamp.
///
///
internal void MoveToPosition (int position)
{
if (Orientation == Orientation.Vertical)
{
Y = _position + SliderPadding / 2;
}
else
{
X = _position + SliderPadding / 2;
}
}
///
/// INTERNAL API (for unit tests) - Clamps the position such that the right side of the slider
/// never goes past the edge of the Viewport.
///
///
///
internal int ClampPosition (int newPosition)
{
return Math.Clamp (newPosition, 0, Math.Max (SliderPadding / 2, VisibleContentSize - SliderPadding - Size));
}
private void RaisePositionChangeEvents (int newPosition)
{
if (OnPositionChanging (_position, newPosition))
{
return;
}
CancelEventArgs args = new (ref _position, ref newPosition);
PositionChanging?.Invoke (this, args);
if (args.Cancel)
{
return;
}
int distance = newPosition - _position;
_position = ClampPosition (newPosition);
MoveToPosition (_position);
OnPositionChanged (_position);
PositionChanged?.Invoke (this, new (in _position));
OnScrolled (distance);
Scrolled?.Invoke (this, new (in distance));
RaiseSelecting (new (Command.Select, null, null, 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;
///
public override Attribute GetNormalColor () { return base.GetHotNormalColor (); }
/////
private int _lastLocation = -1;
///
/// Gets or sets the amount to pad the start and end of the scroll slider. The default is 0.
///
///
/// When the scroll slider is used by , which has increment and decrement buttons, the
/// SliderPadding should be set to the size of the buttons (typically 2).
///
public int SliderPadding { get; set; }
///
protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
{
if (SuperView is null)
{
return false;
}
if (mouseEvent.IsSingleDoubleOrTripleClicked)
{
return true;
}
int location = (Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X);
int offsetFromLastLocation = _lastLocation > -1 ? location - _lastLocation : 0;
int superViewDimension = VisibleContentSize;
if (mouseEvent.IsPressed || mouseEvent.IsReleased)
{
if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1)
{
if (Application.MouseGrabView != this)
{
Application.GrabMouse (this);
_lastLocation = location;
}
}
else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
{
int currentLocation;
if (Orientation == Orientation.Vertical)
{
currentLocation = Frame.Y;
}
else
{
currentLocation = Frame.X;
}
currentLocation -= SliderPadding / 2;
int newLocation = currentLocation + offsetFromLastLocation;
Position = newLocation;
}
else if (mouseEvent.Flags == MouseFlags.Button1Released)
{
_lastLocation = -1;
if (Application.MouseGrabView == this)
{
Application.UngrabMouse ();
}
}
return true;
}
return false;
}
///
/// Gets the slider size.
///
/// The size of the content.
/// The size of the visible content.
/// The bounds of the area the slider moves in (e.g. the size of the minus 2).
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);
}
///
/// Calculates the slider position.
///
/// The size of the content.
/// The size of the visible content.
/// The position in the content (between 0 and ).
/// The bounds of the area the slider moves in (e.g. the size of the minus 2).
/// The direction the slider is moving.
internal 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);
}
///
/// Calculates the content position.
///
/// The size of the content.
/// The size of the visible content.
/// The position of the slider.
/// The bounds of the area the slider moves in (e.g. the size of the minus 2).
internal 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));
}
///
public bool EnableForDesign ()
{
Size = 5;
return true;
}
}