#nullable enable using System.ComponentModel; 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 Scroll : View, IOrientation, IDesignable { internal readonly ScrollSlider _slider; /// public Scroll () { _slider = new (); Add (_slider); _slider.FrameChanged += OnSliderOnFrameChanged; 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); } /// protected override void OnSubviewLayout (LayoutEventArgs args) { if (ViewportDimension < 1) { _slider.Size = 1; return; } _slider.Size = (int)Math.Clamp (Math.Floor ((double)ViewportDimension * ViewportDimension / (Size)), 1, ViewportDimension); } #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; } #endregion /// /// 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 ViewportDimension => Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width; private int _size; /// /// Gets or sets the total size of the content that can be scrolled. /// public int Size { get => _size; set { if (value == _size || value < 0) { return; } _size = value; OnSizeChanged (_size); SizeChanged?.Invoke (this, new (in _size)); SetNeedsLayout (); } } /// Called when has changed. protected virtual void OnSizeChanged (int size) { } /// Raised when has changed. public event EventHandler>? SizeChanged; #region SliderPosition private void OnSliderOnFrameChanged (object? sender, EventArgs args) { if (ViewportDimension == 0) { return; } int framePos = Orientation == Orientation.Vertical ? args.CurrentValue.Y : args.CurrentValue.X; SliderPosition = framePos; } /// /// Gets or sets the position of the start of the Scroll slider, within the Viewport. /// public int SliderPosition { get => CalculateSliderPosition (_contentPosition); set => RaiseSliderPositionChangeEvents (value); } private void RaiseSliderPositionChangeEvents (int newSliderPosition) { int currentSliderPosition = CalculateSliderPosition (_contentPosition); if (newSliderPosition > Size - ViewportDimension || currentSliderPosition == newSliderPosition) { return; } if (OnSliderPositionChanging (currentSliderPosition, newSliderPosition)) { return; } CancelEventArgs args = new (ref currentSliderPosition, ref newSliderPosition); SliderPositionChanging?.Invoke (this, args); if (args.Cancel) { return; } // This sets the slider position and clamps the value _slider.Position = newSliderPosition; ContentPosition = (int)Math.Round ((double)newSliderPosition / (ViewportDimension - _slider.Size) * (Size - ViewportDimension)); OnSliderPositionChanged (newSliderPosition); SliderPositionChanged?.Invoke (this, new (in newSliderPosition)); } /// /// Called when is changing. Return true to cancel the change. /// protected virtual bool OnSliderPositionChanging (int currentSliderPosition, int newSliderPosition) { return false; } /// /// Raised when the is changing. Set to /// to prevent the position from being changed. /// public event EventHandler>? SliderPositionChanging; /// Called when has changed. protected virtual void OnSliderPositionChanged (int position) { } /// Raised when the has changed. public event EventHandler>? SliderPositionChanged; private int CalculateSliderPosition (int contentPosition) { if (Size - ViewportDimension == 0) { return 0; } return (int)Math.Round ((double)contentPosition / (Size - ViewportDimension) * (ViewportDimension - _slider.Size)); } #endregion SliderPosition #region ContentPosition private int _contentPosition; /// /// Gets or sets the position of the ScrollSlider within the range of 0.... /// public int ContentPosition { get => _contentPosition; set { if (value == _contentPosition) { return; } RaiseContentPositionChangeEvents (value); } } private void RaiseContentPositionChangeEvents (int newContentPosition) { // Clamp the value between 0 and Size - ViewportDimension newContentPosition = (int)Math.Clamp (newContentPosition, 0, Math.Max (0, Size - ViewportDimension)); if (OnContentPositionChanging (_contentPosition, newContentPosition)) { return; } CancelEventArgs args = new (ref _contentPosition, ref newContentPosition); ContentPositionChanging?.Invoke (this, args); if (args.Cancel) { return; } _contentPosition = newContentPosition; SliderPosition = CalculateSliderPosition (_contentPosition); OnContentPositionChanged (_contentPosition); ContentPositionChanged?.Invoke (this, new (in _contentPosition)); } /// /// Called when is changing. Return true to cancel the change. /// protected virtual bool OnContentPositionChanging (int currentPos, int newPos) { return false; } /// /// Raised when the is changing. Set to /// to prevent the position from being changed. /// public event EventHandler>? ContentPositionChanging; /// Called when has changed. protected virtual void OnContentPositionChanged (int position) { } /// Raised when the has changed. public event EventHandler>? ContentPositionChanged; #endregion ContentPosition /// protected override bool OnClearingViewport () { FillRect (Viewport, Glyphs.Stipple); return true; } /// protected override bool OnMouseClick (MouseEventArgs args) { if (!args.IsSingleClicked) { return false; } if (Orientation == Orientation.Vertical) { // If the position is w/in the slider frame ignore if (args.Position.Y >= _slider.Frame.Y && args.Position.Y < _slider.Frame.Y + _slider.Frame.Height) { return false; } SliderPosition = args.Position.Y; } else { // If the position is w/in the slider frame ignore if (args.Position.X >= _slider.Frame.X && args.Position.X < _slider.Frame.X + _slider.Frame.Width) { return false; } SliderPosition = args.Position.X; } return true; } /// /// Gets or sets the amount each mouse hweel event will incremenet/decrement the . /// /// /// The default is 1. /// public int Increment { get; set; } = 1; /// 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)) { ContentPosition += Increment; } if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp)) { ContentPosition -= Increment; } } else { if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight)) { ContentPosition += Increment; } if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft)) { ContentPosition -= 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 (); Size = 1000; ContentPosition = 10; return true; } }