#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 (); base.Add (_slider); _slider.Scroll += SliderOnScroll; _slider.PositionChanged += SliderOnPositionChanged; 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; /// /// Gets or sets the size of the viewport into the content being scrolled, bounded by . /// /// /// If not explicitly set, will be the appropriate dimension of the Scroll's Frame. /// public int ViewportDimension { get { if (_viewportDimension.HasValue) { return _viewportDimension.Value; } return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width; } set => _viewportDimension = value; } private int _size; /// /// Gets or sets the 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 SliderOnPositionChanged (object? sender, EventArgs e) { if (ViewportDimension == 0) { return; } int calculatedSliderPos = CalculateSliderPosition (_contentPosition); ContentPosition = (int)Math.Round ((double)e.CurrentValue / (ViewportDimension - _slider.Size) * (Size - ViewportDimension)); RaiseSliderPositionChangeEvents (calculatedSliderPos, e.CurrentValue); } private void SliderOnScroll (object? sender, EventArgs e) { if (ViewportDimension == 0) { return; } } /// /// Gets or sets the position of the start of the Scroll slider, within the Viewport. /// public int GetSliderPosition () => CalculateSliderPosition (_contentPosition); private void RaiseSliderPositionChangeEvents (int calculatedSliderPosition, int newSliderPosition) { if (/*newSliderPosition > Size - ViewportDimension ||*/ calculatedSliderPosition == newSliderPosition) { return; } // This sets the slider position and clamps the value _slider.Position = 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; 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 slider relative to . /// /// /// /// The content position is clamped to 0 and minus . /// /// /// Setting will result in the and /// events being raised. /// /// public int ContentPosition { get => _contentPosition; set { if (value == _contentPosition) { return; } // Clamp the value between 0 and Size - ViewportDimension int newContentPosition = (int)Math.Clamp (value, 0, Math.Max (0, Size - ViewportDimension)); RaiseContentPositionChangeEvents (newContentPosition); _slider.SetPosition (CalculateSliderPosition (_contentPosition)); } } private void RaiseContentPositionChangeEvents (int newContentPosition) { if (OnContentPositionChanging (_contentPosition, newContentPosition)) { return; } CancelEventArgs args = new (ref _contentPosition, ref newContentPosition); ContentPositionChanging?.Invoke (this, args); if (args.Cancel) { return; } _contentPosition = newContentPosition; 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) { // Check if the mouse click is a single click if (!args.IsSingleClicked) { return false; } int sliderCenter; int distanceFromCenter; if (Orientation == Orientation.Vertical) { sliderCenter = _slider.Frame.Y + _slider.Frame.Height / 2; distanceFromCenter = args.Position.Y - sliderCenter; } else { sliderCenter = _slider.Frame.X + _slider.Frame.Width / 2; distanceFromCenter = args.Position.X - sliderCenter; } // Ratio of the distance to the viewport dimension double ratio = (double)Math.Abs (distanceFromCenter) / ViewportDimension; // Jump size based on the ratio and the total content size int jump = (int)Math.Ceiling (ratio * Size); // Adjust the content position based on the distance ContentPosition += distanceFromCenter < 0 ? -jump : jump; 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 = 250; return true; } }