#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. /// /// /// /// If is set, it will be displayed centered within the slider. Set /// to automatically have the Text /// be show what percent the slider is to the Superview's Viewport size. /// /// /// 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; FrameChanged += OnFrameChanged; SubviewLayout += (sender, args) => { }; SubviewsLaidOut += (sender, args) => { if (Orientation == Orientation.Vertical) { if (SuperView?.Viewport.Height > 0) { ViewportDimension = SuperView!.Viewport.Height; } } else { if (SuperView?.Viewport.Width > 0) { ViewportDimension = SuperView!.Viewport.Width; } } if (NeedsLayout) { Layout (); } }; } #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 Size to 1 when changing orientation if (Orientation == Orientation.Vertical) { Width = Dim.Fill (); Size = 1; } else { Height = Dim.Fill (); Size = 1; } 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 bool _showPercent; /// /// Gets or sets whether the ScrollSlider will set to show the percentage the slider /// takes up within the 's Viewport. /// public bool ShowPercent { get => _showPercent; set { _showPercent = value; SetNeedsDraw (); } } 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 clamed 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; set { if (value == _size) { return; } _size = Math.Clamp (value, 1, ViewportDimension); if (Orientation == Orientation.Vertical) { Height = _size; } else { Width = _size; } SetNeedsLayout (); } } private int? _viewportDimension; /// /// 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 ViewportDimension { get { if (_viewportDimension.HasValue) { return _viewportDimension.Value; } return Math.Max (1, Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 2048 : SuperView?.Viewport.Width ?? 2048); } set { if (value == _viewportDimension) { return; } _viewportDimension = int.Max (1, value); if (Size > _viewportDimension) { Size = _viewportDimension.Value; } if (_position > _viewportDimension - _size) { Position = _position; } SetNeedsLayout (); } } private void OnFrameChanged (object? sender, EventArgs e) { //ViewportDimension = (Orientation == Orientation.Vertical ? e.CurrentValue.Height : e.CurrentValue.Width); //Position = (Orientation == Orientation.Vertical ? e.CurrentValue.Y : e.CurrentValue.X) - ShrinkBy / 2; } 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 + ShrinkBy / 2; } else { X = _position + ShrinkBy / 2; } //SetNeedsLayout (); // Layout (); } /// /// 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 (0, ViewportDimension - 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; /// protected override bool OnDrawingText () { if (!ShowPercent) { Text = string.Empty; return false; } if (SuperView is null) { return false; } if (Orientation == Orientation.Vertical) { Text = $"{(int)Math.Round ((double)Viewport.Height / SuperView!.GetContentSize ().Height * 100)}%"; } else { Text = $"{(int)Math.Round ((double)Viewport.Width / SuperView!.GetContentSize ().Width * 100)}%"; } return false; } /// public override Attribute GetNormalColor () { return base.GetHotNormalColor (); } ///// private int _lastLocation = -1; public int ShrinkBy { 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) + ShrinkBy / 2; int offset = _lastLocation > -1 ? location - _lastLocation : 0; int superViewDimension = ViewportDimension; 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)) { if (Orientation == Orientation.Vertical) { Y = Frame.Y + offset < ShrinkBy / 2 ? ShrinkBy / 2 : Frame.Y + offset + Frame.Height > superViewDimension ? Math.Max (superViewDimension - Frame.Height + ShrinkBy, 1) : Frame.Y + offset; } else { X = Frame.X + offset < ShrinkBy / 2 ? ShrinkBy / 2 : Frame.X + offset + Frame.Height > superViewDimension ? Math.Max (superViewDimension - Frame.Height + ShrinkBy / 2, 1) : Frame.X + offset; } } else if (mouseEvent.Flags == MouseFlags.Button1Released) { _lastLocation = -1; if (Application.MouseGrabView == this) { Application.UngrabMouse (); } } return true; } return false; } /// public bool EnableForDesign () { OrientationChanged += (sender, args) => { if (args.CurrentValue == Orientation.Vertical) { Width = Dim.Fill (); Size = 5; } else { Size = 5; Height = Dim.Fill (); } }; Orientation = Orientation.Horizontal; ShowPercent = true; return true; } }