#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; } }