123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- #nullable enable
- using System.ComponentModel;
- namespace Terminal.Gui;
- /// <summary>
- /// Indicates the size of scrollable content and controls the position of the visible content, either vertically or
- /// horizontally.
- /// Two <see cref="Button"/>s are provided, one to scroll up or left and one to scroll down or right. Between the
- /// buttons is a <see cref="ScrollSlider"/> that can be dragged to
- /// control the position of the visible content. The ScrollSlier is sized to show the proportion of the scrollable
- /// content to the size of the <see cref="View.Viewport"/>.
- /// </summary>
- /// <remarks>
- /// <para>
- /// See the <see href="https://gui-cs.github.io/Terminal.GuiV2Docs/docs/scrolling.html">Scrolling Deep Dive</see>.
- /// </para>
- /// <para>
- /// By default, the built-in View scrollbars (<see cref="View.VerticalScrollBar"/>/
- /// <see cref="View.HorizontalScrollBar"/>) have both <see cref="View.Visible"/> and <see cref="AutoShow"/> set to
- /// <see langword="false"/>.
- /// To enable them, either set <see cref="AutoShow"/> set to <see langword="true"/> or explicitly set
- /// <see cref="View.Visible"/>
- /// to <see langword="true"/>.
- /// </para>
- /// <para>
- /// By default, this view cannot be focused and does not support keyboard input.
- /// </para>
- /// </remarks>
- public class ScrollBar : View, IOrientation, IDesignable
- {
- private readonly Button _decreaseButton;
- private readonly ScrollSlider _slider;
- private readonly Button _increaseButton;
- /// <inheritdoc/>
- public ScrollBar ()
- {
- // Set the default width and height based on the orientation - fill Viewport
- Width = Dim.Auto (
- DimAutoStyle.Content,
- Dim.Func (() => Orientation == Orientation.Vertical ? 1 : SuperView?.Viewport.Width ?? 0));
- Height = Dim.Auto (
- DimAutoStyle.Content,
- Dim.Func (() => Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 0 : 1));
- _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;
- Add (_decreaseButton, _slider, _increaseButton);
- CanFocus = false;
- _orientationHelper = new (this); // Do not use object initializer!
- _orientationHelper.Orientation = Orientation.Vertical;
- // This sets the width/height etc...
- OnOrientationChanged (Orientation);
- return;
- 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) { ShowHide (); }
- private void ShowHide ()
- {
- if (AutoShow)
- {
- Visible = VisibleContentSize < ScrollableContentSize;
- }
- _slider.VisibleContentSize = VisibleContentSize;
- _slider.Size = CalculateSliderSize ();
- _sliderPosition = CalculateSliderPositionFromContentPosition (_position);
- _slider.Position = _sliderPosition.Value;
- }
- 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;
- }
- #pragma warning disable CS0067 // The event is never used
- /// <inheritdoc/>
- public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
- /// <inheritdoc/>
- public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
- #pragma warning restore CS0067 // The event is never used
- /// <inheritdoc/>
- public void OnOrientationChanged (Orientation newOrientation)
- {
- TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
- TextAlignment = Alignment.Center;
- VerticalTextAlignment = Alignment.Center;
- _slider.Orientation = newOrientation;
- PositionSubviews ();
- OrientationChanged?.Invoke (this, new (newOrientation));
- }
- #endregion
- /// <summary>
- /// Gets or sets the amount each mouse wheel event, or click on the increment/decrement buttons, will
- /// incremenet/decrement the <see cref="Position"/>.
- /// </summary>
- /// <remarks>
- /// The default is 1.
- /// </remarks>
- public int Increment { get; set; } = 1;
- // AutoShow should be false by default. Views should not be hidden by default.
- private bool _autoShow;
- /// <summary>
- /// Gets or sets whether <see cref="View.Visible"/> will be set to <see langword="true"/> if the dimension of the
- /// scroll bar is less than <see cref="ScrollableContentSize"/> and <see langword="false"/> if greater than or equal
- /// to.
- /// </summary>
- /// <remarks>
- /// The default is <see langword="false"/>.
- /// </remarks>
- public bool AutoShow
- {
- get => _autoShow;
- set
- {
- if (_autoShow != value)
- {
- _autoShow = value;
- if (!AutoShow)
- {
- Visible = true;
- }
- ShowHide ();
- SetNeedsLayout ();
- }
- }
- }
- 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, the visible content size will be appropriate dimension of the ScrollBar'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 ();
- ShowHide ();
- }
- }
- 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
- {
- if (_scrollableContentSize.HasValue)
- {
- return _scrollableContentSize.Value;
- }
- return Orientation == Orientation.Vertical ? SuperView?.GetContentSize ().Height ?? 0 : SuperView?.GetContentSize ().Width ?? 0;
- }
- set
- {
- if (value == _scrollableContentSize || value < 0)
- {
- return;
- }
- _scrollableContentSize = value;
- _slider.Size = CalculateSliderSize ();
- ShowHide ();
- if (!Visible)
- {
- return;
- }
- OnSizeChanged (value);
- ScrollableContentSizeChanged?.Invoke (this, new (in value));
- 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 || !Visible)
- {
- return;
- }
- // Clamp the value between 0 and Size - VisibleContentSize
- int newContentPosition = 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;
- if (_position == newContentPosition)
- {
- return;
- }
- _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));
- SetNeedsLayout ();
- }
- }
- /// <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 () { return 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
- // TODO: This logic mostly works to provide a proportional jump. However, the math
- // TODO: falls apart in edge cases. Most other scroll bars (e.g. Windows) do not do proportional
- // TODO: Thus, this is disabled; 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;
- }
- }
|