diff --git a/Terminal.Gui/Views/Scroll/Scroll.cs b/Terminal.Gui/Views/Scroll/Scroll.cs index 42a3da02c..118e75e71 100644 --- a/Terminal.Gui/Views/Scroll/Scroll.cs +++ b/Terminal.Gui/Views/Scroll/Scroll.cs @@ -1,7 +1,6 @@ #nullable enable using System.ComponentModel; -using System.Drawing; namespace Terminal.Gui; @@ -39,8 +38,7 @@ public class Scroll : View, IOrientation, IDesignable OnOrientationChanged (Orientation); } - - /// + /// protected override void OnSubviewLayout (LayoutEventArgs args) { if (ViewportDimension < 1) @@ -49,10 +47,12 @@ public class Scroll : View, IOrientation, IDesignable return; } - _slider.Size = (int)Math.Clamp (Math.Floor ((double)ViewportDimension * ViewportDimension / (Size)), 1, ViewportDimension); + + _slider.Size = (int)Math.Clamp (Math.Floor ((double)ViewportDimension * ViewportDimension / (Size - 2)), 1, ViewportDimension); } #region IOrientation members + private readonly OrientationHelper _orientationHelper; /// @@ -133,16 +133,16 @@ public class Scroll : View, IOrientation, IDesignable /// Raised when has changed. public event EventHandler>? SizeChanged; - private int _sliderPosition; - + #region SliderPosition private void OnSliderOnFrameChanged (object? sender, EventArgs args) { if (ViewportDimension == 0) { return; } + int framePos = Orientation == Orientation.Vertical ? args.CurrentValue.Y : args.CurrentValue.X; - RaisePositionChangeEvents (_sliderPosition, framePos); + SliderPosition = framePos; } /// @@ -150,57 +150,23 @@ public class Scroll : View, IOrientation, IDesignable /// public int SliderPosition { - get => _sliderPosition; - set => RaisePositionChangeEvents (_sliderPosition, value); + get => CalculateSliderPosition (_contentPosition); + set => RaiseSliderPositionChangeEvents (value); } - /// - /// Gets or sets the position of the ScrollSlider within the range of 0.... - /// - public int ContentPosition + private void RaiseSliderPositionChangeEvents (int newSliderPosition) { - get + int currentSliderPosition = CalculateSliderPosition (_contentPosition); + + if (newSliderPosition > Size - ViewportDimension) { - if (ViewportDimension - _slider.Size == 0) - { - return 0; - } - return (int)Math.Round (GetCurrentContentPosition ()); + return; } - set - { - if (Size - ViewportDimension == 0) - { - SliderPosition = 0; - return; - } - double newContentPos = (double)value / (ViewportDimension - _slider.Size) * (Size - ViewportDimension); - double newSliderPos = (double)value / (Size - ViewportDimension) * (ViewportDimension - _slider.Size); - - int newSliderPosition = (int)Math.Ceiling (newSliderPos); - if (newContentPos >= GetCurrentContentPosition ()) - { - newSliderPosition = (int)Math.Floor (newSliderPos); - } - - if (SliderPosition != newSliderPosition) - { - SliderPosition = newSliderPosition; - } - } - } - - private double GetCurrentContentPosition () - { - return (double)SliderPosition / (ViewportDimension - _slider.Size) * (Size - ViewportDimension); - } - - private void RaisePositionChangeEvents (int currentSliderPosition, int newSliderPosition) - { - if (OnPositionChanging (currentSliderPosition, newSliderPosition)) + if (OnSliderPositionChanging (currentSliderPosition, newSliderPosition)) { _slider.Position = currentSliderPosition; + return; } @@ -210,30 +176,28 @@ public class Scroll : View, IOrientation, IDesignable if (args.Cancel) { _slider.Position = currentSliderPosition; + return; } // This sets the slider position and clamps the value _slider.Position = newSliderPosition; - if (_slider.Position == _sliderPosition) + if (_slider.Position == currentSliderPosition) { return; } - _sliderPosition = newSliderPosition; + ContentPosition = (int)Math.Round ((double)newSliderPosition / (ViewportDimension - _slider.Size) * (Size - ViewportDimension)); - OnPositionChanged (_sliderPosition); + OnSliderPositionChanged (newSliderPosition); SliderPositionChanged?.Invoke (this, new (in newSliderPosition)); } /// /// Called when is changing. Return true to cancel the change. /// - protected virtual bool OnPositionChanging (int currentPos, int newPos) - { - return false; - } + protected virtual bool OnSliderPositionChanging (int currentSliderPosition, int newSliderPosition) { return false; } /// /// Raised when the is changing. Set to @@ -242,11 +206,88 @@ public class Scroll : View, IOrientation, IDesignable public event EventHandler>? SliderPositionChanging; /// Called when has changed. - protected virtual void OnPositionChanged (int position) { } + 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, 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 () @@ -256,7 +297,7 @@ public class Scroll : View, IOrientation, IDesignable return true; } - /// + /// protected override bool OnMouseClick (MouseEventArgs args) { if (!args.IsSingleClicked) @@ -285,7 +326,6 @@ public class Scroll : View, IOrientation, IDesignable SliderPosition = args.Position.X; } - return true; } @@ -306,29 +346,31 @@ public class Scroll : View, IOrientation, IDesignable { if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown)) { - SliderPosition++; + ContentPosition++; } + if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp)) { - SliderPosition--; + ContentPosition--; } } else { if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight)) { - SliderPosition++; + ContentPosition++; } + if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft)) { - SliderPosition--; + ContentPosition--; } } return true; } - /// + /// public bool EnableForDesign () { OrientationChanged += (sender, args) => @@ -348,9 +390,7 @@ public class Scroll : View, IOrientation, IDesignable Width = 1; Height = Dim.Fill (); Size = 1000; - SliderPosition = 10; - - + ContentPosition = 10; return true; } diff --git a/Terminal.Gui/Views/Scroll/ScrollBar.cs b/Terminal.Gui/Views/Scroll/ScrollBar.cs index 334cd5cf2..1ee3abc01 100644 --- a/Terminal.Gui/Views/Scroll/ScrollBar.cs +++ b/Terminal.Gui/Views/Scroll/ScrollBar.cs @@ -29,6 +29,8 @@ public class ScrollBar : View, IOrientation, IDesignable _scroll = new (); _scroll.SliderPositionChanging += OnScrollOnSliderPositionChanging; _scroll.SliderPositionChanged += OnScrollOnSliderPositionChanged; + _scroll.ContentPositionChanging += OnScrollOnContentPositionChanging; + _scroll.ContentPositionChanged += OnScrollOnContentPositionChanged; _scroll.SizeChanged += OnScrollOnSizeChanged; _decreaseButton = new () @@ -64,13 +66,13 @@ public class ScrollBar : View, IOrientation, IDesignable void OnDecreaseButtonOnAccept (object? s, CommandEventArgs e) { - _scroll.ContentPosition--; + ContentPosition -= Increment; e.Cancel = true; } void OnIncreaseButtonOnAccept (object? s, CommandEventArgs e) { - _scroll.ContentPosition++; + ContentPosition += Increment; e.Cancel = true; } } @@ -188,8 +190,8 @@ public class ScrollBar : View, IOrientation, IDesignable set => _scroll.SliderPosition = value; } - private void OnScrollOnSliderPositionChanged (object? sender, EventArgs e) { SliderPositionChanged?.Invoke (this, e); } private void OnScrollOnSliderPositionChanging (object? sender, CancelEventArgs e) { SliderPositionChanging?.Invoke (this, e); } + private void OnScrollOnSliderPositionChanged (object? sender, EventArgs e) { SliderPositionChanged?.Invoke (this, e); } /// /// Raised when the is changing. Set to @@ -219,6 +221,18 @@ public class ScrollBar : View, IOrientation, IDesignable set => _scroll.ContentPosition = value; } + private void OnScrollOnContentPositionChanging (object? sender, CancelEventArgs e) { ContentPositionChanging?.Invoke (this, e); } + private void OnScrollOnContentPositionChanged (object? sender, EventArgs e) { ContentPositionChanged?.Invoke (this, e); } + + /// + /// Raised when the is changing. Set to + /// to prevent the position from being changed. + /// + public event EventHandler>? ContentPositionChanging; + + /// Raised when the has changed. + public event EventHandler>? ContentPositionChanged; + /// Raised when has changed. public event EventHandler>? SizeChanged; @@ -228,6 +242,14 @@ public class ScrollBar : View, IOrientation, IDesignable SizeChanged?.Invoke (this, e); } + /// + /// Gets or sets the amount each click of the increment/decrement buttons will incremenet/decrement the . + /// + /// + /// The default is 1. + /// + public int Increment { get; set; } = 1; + /// protected override void OnSubviewLayout (LayoutEventArgs args) { PositionSubviews (); } diff --git a/UICatalog/Scenarios/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap.cs index f069ca457..5f513f3d6 100644 --- a/UICatalog/Scenarios/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap.cs @@ -10,9 +10,7 @@ using System.Text; using System.Text.Json; using System.Text.Unicode; using System.Threading.Tasks; -using JetBrains.Annotations; using Terminal.Gui; -using static System.Runtime.InteropServices.JavaScript.JSType; using static Terminal.Gui.SpinnerStyle; namespace UICatalog.Scenarios; @@ -303,7 +301,7 @@ public class CharacterMap : Scenario public override List GetDemoKeyStrokes () { - List keys = new List (); + List keys = new (); for (var i = 0; i < 200; i++) { @@ -345,15 +343,17 @@ internal class CharMap : View, IDesignable ColorScheme = Colors.ColorSchemes ["Dialog"]; CanFocus = true; CursorVisibility = CursorVisibility.Default; + //ViewportSettings = ViewportSettings.AllowLocationGreaterThanContentSize; - SetContentSize (new (COLUMN_WIDTH * 16, (_maxCodePoint / 16) * _rowHeight)); // +1 for Header + SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, _maxCodePoint / 16 * _rowHeight)); // +1 for Header AddCommand ( Command.Up, () => { SelectedCodePoint -= 16; + return true; } ); @@ -363,6 +363,7 @@ internal class CharMap : View, IDesignable () => { SelectedCodePoint += 16; + return true; } ); @@ -372,6 +373,7 @@ internal class CharMap : View, IDesignable () => { SelectedCodePoint--; + return true; } ); @@ -380,7 +382,8 @@ internal class CharMap : View, IDesignable Command.Right, () => { - SelectedCodePoint++; + SelectedCodePoint++; + return true; } ); @@ -455,11 +458,12 @@ internal class CharMap : View, IDesignable _hScrollBar = new () { AutoHide = false, - X = RowLabelWidth + 1, + X = RowLabelWidth, Y = Pos.AnchorEnd (), Orientation = Orientation.Horizontal, Width = Dim.Fill (1), - Size = GetContentSize ().Width + Size = GetContentSize ().Width - RowLabelWidth, + Increment = COLUMN_WIDTH }; _vScrollBar = new () @@ -473,26 +477,31 @@ internal class CharMap : View, IDesignable Padding.Add (_vScrollBar, _hScrollBar); - _vScrollBar.SliderPositionChanged += (sender, args) => - { - if (Viewport.Height > 0) - { - Viewport = Viewport with { Y = _vScrollBar.ContentPosition }; - } - }; + _vScrollBar.ContentPositionChanged += (sender, args) => + { + if (Viewport.Height > 0) + { + Viewport = Viewport with + { + Y = Math.Min (args.CurrentValue, GetContentSize ().Height - (Viewport.Height - 2)) + }; + } + }; - _hScrollBar.SliderPositionChanged += (sender, args) => - { - if (Viewport.Width > 0) - { - Viewport = Viewport with { X = _hScrollBar.ContentPosition }; - } - }; + _hScrollBar.ContentPositionChanged += (sender, args) => + { + if (Viewport.Width > 0) + { + Viewport = Viewport with + { + X = Math.Min (args.CurrentValue, GetContentSize ().Width - Viewport.Width) + }; + } + }; FrameChanged += (sender, args) => { - int width = Viewport.Width / COLUMN_WIDTH * COLUMN_WIDTH - RowLabelWidth; - if (width < GetContentSize ().Width) + if (Viewport.Width < GetContentSize ().Width) { Padding.Thickness = Padding.Thickness with { Bottom = 1 }; } @@ -500,6 +509,7 @@ internal class CharMap : View, IDesignable { Padding.Thickness = Padding.Thickness with { Bottom = 0 }; } + _hScrollBar.ContentPosition = Viewport.X; _vScrollBar.ContentPosition = Viewport.Y; }; @@ -527,7 +537,8 @@ internal class CharMap : View, IDesignable if (e.Flags == MouseFlags.WheeledRight) { - _hScrollBar.SliderPosition++; + ScrollHorizontal (1); + _hScrollBar.ContentPosition = Viewport.X; e.Handled = true; return; @@ -535,7 +546,8 @@ internal class CharMap : View, IDesignable if (e.Flags == MouseFlags.WheeledLeft) { - _hScrollBar.SliderPosition--; + ScrollHorizontal (-1); + _hScrollBar.ContentPosition = Viewport.X; e.Handled = true; } } @@ -545,8 +557,8 @@ internal class CharMap : View, IDesignable { get { - int row = SelectedCodePoint / 16 * _rowHeight - Viewport.Y + 1; // + 1 for header - int col = SelectedCodePoint % 16 * COLUMN_WIDTH - Viewport.X + RowLabelWidth + 1; // + 1 for padding between label and first column + int row = SelectedCodePoint / 16 * _rowHeight + 1 - Viewport.Y; // + 1 for header + int col = SelectedCodePoint % 16 * COLUMN_WIDTH + RowLabelWidth + 1 - Viewport.X; // + 1 for padding between label and first column return new (col, row); } @@ -556,7 +568,8 @@ internal class CharMap : View, IDesignable public static int _maxCodePoint = UnicodeRange.Ranges.Max (r => r.End); /// - /// Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point visible. + /// Gets or sets the currently selected codepoint. Causes the Viewport to scroll to make the selected code point + /// visible. /// public int SelectedCodePoint { @@ -571,58 +584,48 @@ internal class CharMap : View, IDesignable Point prevCursor = Cursor; int newSelectedCodePoint = Math.Clamp (value, 0, _maxCodePoint); - ScrollToMakeRowVisible (newSelectedCodePoint / 16 * _rowHeight, prevCursor.Y); - ScrollToMakeColumnVisible (newSelectedCodePoint % 16 * COLUMN_WIDTH, prevCursor.X); - - if (_selected != newSelectedCodePoint) + Point newCursor = new () { - _selected = newSelectedCodePoint; - SetNeedsDraw (); - SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null)); - } + X = newSelectedCodePoint % 16 * COLUMN_WIDTH + RowLabelWidth + 1 - Viewport.X, + Y = newSelectedCodePoint / 16 * _rowHeight + 1 - Viewport.Y + }; + + // Ensure the new cursor position is visible + EnsureCursorIsVisible (newCursor); + + _selected = newSelectedCodePoint; + SetNeedsDraw (); + SelectedCodePointChanged?.Invoke (this, new (SelectedCodePoint, null)); } } - private void ScrollToMakeRowVisible (int row, int prevCursorY) + private void EnsureCursorIsVisible (Point newCursor) { - int height = Viewport.Height - 1; // Header - int delta = row - (Viewport.Y + height); - int scroll = Viewport.Height - (prevCursorY - delta); + // Adjust vertical scrolling + if (newCursor.Y < 1) // Header is at Y = 0 + { + ScrollVertical (newCursor.Y - 1); + } + else if (newCursor.Y >= Viewport.Height) + { + ScrollVertical (newCursor.Y - Viewport.Height + 1); + } - if (row - Viewport.Y < 0) + _vScrollBar.ContentPosition = Viewport.Y; + + // Adjust horizontal scrolling + if (newCursor.X < RowLabelWidth + 1) { - // Moving up. - Viewport = Viewport with { Y = Viewport.Y + (row - Viewport.Y) }; + ScrollHorizontal (newCursor.X - (RowLabelWidth + 1)); } - else if (row - Viewport.Y >= height) + else if (newCursor.X >= Viewport.Width) { - // Moving down. - Viewport = Viewport with { Y = Math.Min (Viewport.Y + scroll, GetContentSize ().Height - height ) }; + ScrollHorizontal (newCursor.X - Viewport.Width + 1); } - _vScrollBar.ContentPosition = row; + + _hScrollBar.ContentPosition = Viewport.X; } - private void ScrollToMakeColumnVisible (int col, int prevCursorX) - { - int width = Viewport.Width - RowLabelWidth; - int delta = col - (Viewport.X + width); - int scroll = Viewport.Width - (prevCursorX - delta); - - if (col - Viewport.X < 0) - { - // Moving left. - Viewport = Viewport with { X = Viewport.X + (col - Viewport.X) }; - _hScrollBar.ContentPosition = col; - } - else if (col - Viewport.X >= width) - { - // Moving right. - Viewport = Viewport with { X = Math.Min (Viewport.X + scroll, GetContentSize ().Width - width) }; - _hScrollBar.ContentPosition = col; - } - } - - public bool ShowGlyphWidths { get => _rowHeight == 2; @@ -997,7 +1000,7 @@ internal class CharMap : View, IDesignable document.RootElement, new JsonSerializerOptions - { WriteIndented = true } + { WriteIndented = true } ); }