mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-01 00:46:39 +01:00
628 lines
20 KiB
C#
628 lines
20 KiB
C#
#nullable enable
|
|
|
|
using System.ComponentModel;
|
|
using System.Drawing;
|
|
|
|
namespace Terminal.Gui;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="View.Viewport"/>. The ScrollSlider
|
|
/// can be dragged with the mouse. A Scroll can be oriented either vertically or horizontally and is used within a
|
|
/// <see cref="ScrollBar"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// By default, this view cannot be focused and does not support keyboard.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class ScrollBar : View, IOrientation, IDesignable
|
|
{
|
|
private readonly Button _decreaseButton;
|
|
internal readonly ScrollSlider _slider;
|
|
private readonly Button _increaseButton;
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
|
|
/// <inheritdoc/>
|
|
public Orientation Orientation
|
|
{
|
|
get => _orientationHelper.Orientation;
|
|
set => _orientationHelper.Orientation = value;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
|
|
|
|
/// <inheritdoc/>
|
|
public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
|
|
|
|
/// <inheritdoc/>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Gets or sets the amount each mouse wheel event will incremenet/decrement the <see cref="Position"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The default is 1.
|
|
/// </remarks>
|
|
public int Increment { get; set; } = 1;
|
|
|
|
private bool _autoHide = true;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether <see cref="View.Visible"/> will be set to <see langword="false"/> if the dimension of the
|
|
/// scroll bar is greater than or equal to <see cref="ScrollableContentSize"/>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the Scroll will show the percentage the slider
|
|
/// takes up within the <see cref="ScrollableContentSize"/>.
|
|
/// </summary>
|
|
public bool ShowPercent
|
|
{
|
|
get => _slider.ShowPercent;
|
|
set => _slider.ShowPercent = value;
|
|
}
|
|
|
|
private int? _visibleContentSize;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the size of the visible viewport into the content being scrolled, bounded by <see cref="ScrollableContentSize"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
|
|
/// </remarks>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the size of the content that can be scrolled. This is typically set to <see cref="View.GetContentSize()"/>.
|
|
/// </summary>
|
|
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 ();
|
|
}
|
|
}
|
|
|
|
/// <summary>Called when <see cref="ScrollableContentSize"/> has changed. </summary>
|
|
protected virtual void OnSizeChanged (int size) { }
|
|
|
|
/// <summary>Raised when <see cref="ScrollableContentSize"/> has changed.</summary>
|
|
public event EventHandler<EventArgs<int>>? ScrollableContentSizeChanged;
|
|
|
|
#region Position
|
|
|
|
private int _position;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the position of the slider relative to <see cref="ScrollableContentSize"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The content position is clamped to 0 and <see cref="ScrollableContentSize"/> minus <see cref="VisibleContentSize"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// Setting will result in the <see cref="PositionChanging"/> and <see cref="PositionChanged"/>
|
|
/// events being raised.
|
|
/// </para>
|
|
/// </remarks>
|
|
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<int> 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));
|
|
}
|
|
}
|
|
|
|
/// <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 OnScrolled (int distance) { }
|
|
|
|
/// <summary>Raised when the <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
|
|
public event EventHandler<EventArgs<int>>? Scrolled;
|
|
|
|
|
|
/// <summary>
|
|
/// INTERNAL API (for unit tests) - Calculates the position within the <see cref="ScrollableContentSize"/> based on the slider position.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Clamps the sliderPosition, ensuring the returned content position is always less than
|
|
/// <see cref="ScrollableContentSize"/> - <see cref="VisibleContentSize"/>.
|
|
/// </remarks>
|
|
/// <param name="sliderPosition"></param>
|
|
/// <returns></returns>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// INTERNAL (for unit tests). Calculates the size of the slider based on the Orientation, VisibleContentSize, the actual Viewport, and Size.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
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<int> e)
|
|
{
|
|
if (VisibleContentSize == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RaiseSliderPositionChangeEvents (_sliderPosition, e.CurrentValue);
|
|
}
|
|
|
|
private void SliderOnScroll (object? sender, EventArgs<int> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the position of the start of the Scroll slider, within the Viewport.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>Called when the slider position has changed.</summary>
|
|
protected virtual void OnSliderPositionChanged (int position) { }
|
|
|
|
/// <summary>Raised when the slider position has changed.</summary>
|
|
public event EventHandler<EventArgs<int>>? SliderPositionChanged;
|
|
|
|
/// <summary>
|
|
/// INTERNAL API (for unit tests) - Calculates the position of the slider based on the content position.
|
|
/// </summary>
|
|
/// <param name="contentPosition"></param>
|
|
/// <param name="direction"></param>
|
|
/// <returns></returns>
|
|
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
|
|
|
|
/// <inheritdoc/>
|
|
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.
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
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;
|
|
}
|
|
}
|