Scroll.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. #nullable enable
  2. using System.ComponentModel;
  3. namespace Terminal.Gui;
  4. /// <summary>
  5. /// Provides a proportional control for scrolling through content. Used within a <see cref="ScrollBar"/>.
  6. /// </summary>
  7. public class Scroll : View
  8. {
  9. /// <inheritdoc/>
  10. public Scroll ()
  11. {
  12. WantContinuousButtonPressed = true;
  13. CanFocus = false;
  14. Orientation = Orientation.Vertical;
  15. Width = Dim.Auto (DimAutoStyle.Content, 1);
  16. Height = Dim.Auto (DimAutoStyle.Content, 1);
  17. _slider = new (this);
  18. Add (_slider);
  19. }
  20. private readonly ScrollSlider _slider;
  21. private Orientation _orientation;
  22. private int _position;
  23. private int _size;
  24. /// <inheritdoc/>
  25. public override void EndInit ()
  26. {
  27. base.EndInit ();
  28. AdjustSlider ();
  29. }
  30. /// <inheritdoc/>
  31. public override void OnAdded (SuperViewChangedEventArgs e)
  32. {
  33. View parent = (e.Parent is Adornment adornment ? adornment.Parent : e.Parent)!;
  34. parent.LayoutComplete += SuperView_LayoutComplete!;
  35. base.OnAdded (e);
  36. }
  37. /// <inheritdoc/>
  38. public override void OnRemoved (SuperViewChangedEventArgs e)
  39. {
  40. if (e.Parent is { })
  41. {
  42. View parent = (e.Parent is Adornment adornment ? adornment.Parent : e.Parent)!;
  43. parent.LayoutComplete -= SuperView_LayoutComplete!;
  44. }
  45. base.OnRemoved (e);
  46. }
  47. /// <summary>
  48. /// Gets or sets if the Scroll is oriented vertically or horizontally.
  49. /// </summary>
  50. public Orientation Orientation
  51. {
  52. get => _orientation;
  53. set
  54. {
  55. _orientation = value;
  56. AdjustSlider ();
  57. }
  58. }
  59. /// <summary>
  60. /// Gets or sets the position of the start of the Scroll slider, relative to <see cref="Size"/>.
  61. /// </summary>
  62. public int Position
  63. {
  64. get => _position;
  65. set
  66. {
  67. if (value == _position || value < 0)
  68. {
  69. return;
  70. }
  71. int barSize = Orientation == Orientation.Vertical ? GetContentSize ().Height : GetContentSize ().Width;
  72. if (value + barSize > Size)
  73. {
  74. return;
  75. }
  76. CancelEventArgs<int> args = OnPositionChanging (_position, value);
  77. if (args.Cancel)
  78. {
  79. return;
  80. }
  81. _position = value;
  82. if (!_slider._wasSliderMouse)
  83. {
  84. AdjustSlider ();
  85. }
  86. OnPositionChanged (_position);
  87. }
  88. }
  89. /// <summary>Raised when the <see cref="Position"/> has changed.</summary>
  90. public event EventHandler<EventArgs<int>>? PositionChanged;
  91. /// <summary>
  92. /// Raised when the <see cref="Position"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
  93. /// <see langword="true"/> to prevent the position from being changed.
  94. /// </summary>
  95. public event EventHandler<CancelEventArgs<int>>? PositionChanging;
  96. /// <summary>
  97. /// Gets or sets the size of the Scroll. This is the total size of the content that can be scrolled through.
  98. /// </summary>
  99. public int Size
  100. {
  101. get => _size;
  102. set
  103. {
  104. _size = value;
  105. OnSizeChanged (_size);
  106. AdjustSlider ();
  107. }
  108. }
  109. /// <summary>Raised when <see cref="Size"/> has changed.</summary>
  110. public event EventHandler<EventArgs<int>>? SizeChanged;
  111. /// <inheritdoc/>
  112. protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
  113. {
  114. int location = Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X;
  115. int barSize = Orientation == Orientation.Vertical ? GetContentSize ().Height : GetContentSize ().Width;
  116. (int topLeft, int bottomRight) sliderPos = _orientation == Orientation.Vertical
  117. ? new (_slider.Frame.Y, _slider.Frame.Bottom - 1)
  118. : new (_slider.Frame.X, _slider.Frame.Right - 1);
  119. if (mouseEvent.Flags == MouseFlags.Button1Pressed && location < sliderPos.topLeft)
  120. {
  121. Position = Math.Max (Position - barSize, 0);
  122. }
  123. else if (mouseEvent.Flags == MouseFlags.Button1Pressed && location > sliderPos.bottomRight)
  124. {
  125. Position = Math.Min (Position + barSize, Size - barSize);
  126. }
  127. else if ((mouseEvent.Flags == MouseFlags.WheeledDown && Orientation == Orientation.Vertical)
  128. || (mouseEvent.Flags == MouseFlags.WheeledRight && Orientation == Orientation.Horizontal))
  129. {
  130. Position = Math.Min (Position + 1, Size - barSize);
  131. }
  132. else if ((mouseEvent.Flags == MouseFlags.WheeledUp && Orientation == Orientation.Vertical)
  133. || (mouseEvent.Flags == MouseFlags.WheeledLeft && Orientation == Orientation.Horizontal))
  134. {
  135. Position = Math.Max (Position - 1, 0);
  136. }
  137. else if (mouseEvent.Flags == MouseFlags.Button1Clicked)
  138. {
  139. if (_slider.Frame.Contains (mouseEvent.Position))
  140. {
  141. return _slider.OnMouseEvent (mouseEvent);
  142. }
  143. }
  144. return base.OnMouseEvent (mouseEvent);
  145. }
  146. // TODO: Move this into "ScrollSlider" and override it there. Scroll can then subscribe to _slider.LayoutComplete and call AdjustSlider.
  147. // QUESTION: I've been meaning to add a "View.FrameChanged" event (fired from LayoutComplete only if Frame has changed). Should we do that as part of this PR?
  148. // QUESTION: Note I *did* add "View.ViewportChanged" in a previous PR.
  149. /// <summary>Virtual method called when <see cref="Position"/> has changed. Raises <see cref="PositionChanged"/>.</summary>
  150. protected virtual void OnPositionChanged (int position) { PositionChanged?.Invoke (this, new (in position)); }
  151. /// <summary>
  152. /// Virtual method called when <see cref="Position"/> is changing. Raises <see cref="PositionChanging"/>, which is
  153. /// cancelable.
  154. /// </summary>
  155. protected virtual CancelEventArgs<int> OnPositionChanging (int currentPos, int newPos)
  156. {
  157. CancelEventArgs<int> args = new (ref currentPos, ref newPos);
  158. PositionChanging?.Invoke (this, args);
  159. return args;
  160. }
  161. /// <summary>Virtual method called when <see cref="Size"/> has changed. Raises <see cref="SizeChanged"/>.</summary>
  162. protected void OnSizeChanged (int size) { SizeChanged?.Invoke (this, new (in size)); }
  163. private void AdjustSlider ()
  164. {
  165. if (!IsInitialized)
  166. {
  167. return;
  168. }
  169. (int Location, int Dimension) slider = GetSliderLocationDimensionFromPosition ();
  170. _slider.X = Orientation == Orientation.Vertical ? 0 : slider.Location;
  171. _slider.Y = Orientation == Orientation.Vertical ? slider.Location : 0;
  172. _slider.SetContentSize (
  173. new (
  174. Orientation == Orientation.Vertical ? GetContentSize ().Width : slider.Dimension,
  175. Orientation == Orientation.Vertical ? slider.Dimension : GetContentSize ().Height
  176. ));
  177. SetSliderText ();
  178. }
  179. // QUESTION: This method is only called from one place. Should it be inlined? Or, should it be made internal and unit tests be provided?
  180. private (int Location, int Dimension) GetSliderLocationDimensionFromPosition ()
  181. {
  182. if (GetContentSize ().Height == 0 || GetContentSize ().Width == 0)
  183. {
  184. return new (0, 0);
  185. }
  186. int scrollSize = Orientation == Orientation.Vertical ? GetContentSize ().Height : GetContentSize ().Width;
  187. int location;
  188. int dimension;
  189. if (Size > 0)
  190. {
  191. dimension = Math.Min (Math.Max (scrollSize * scrollSize / Size, 1), scrollSize);
  192. // Ensure the Position is valid
  193. if (Position > 0 && Position + scrollSize > Size)
  194. {
  195. Position = Size - scrollSize;
  196. }
  197. location = Math.Min ((Position * scrollSize + Position) / Size, scrollSize - dimension);
  198. if (Position == Size - scrollSize && location + dimension < scrollSize)
  199. {
  200. location = scrollSize - dimension;
  201. }
  202. }
  203. else
  204. {
  205. location = 0;
  206. dimension = scrollSize;
  207. }
  208. return new (location, dimension);
  209. }
  210. // TODO: I think you should create a new `internal` view named "ScrollSlider" with an `Orientation` property. It should inherit from View and override GetNormalColor and the mouse events
  211. // that can be moved within it's Superview, constrained to move only horizontally or vertically depending on Orientation.
  212. // This will really simplify a lot of this.
  213. private void SetSliderText ()
  214. {
  215. TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
  216. // QUESTION: Should these Glyphs be configurable via CM?
  217. Text = string.Concat (
  218. Enumerable.Repeat (
  219. Glyphs.Stipple.ToString (),
  220. GetContentSize ().Width * GetContentSize ().Height));
  221. _slider.TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
  222. _slider.Text = string.Concat (
  223. Enumerable.Repeat (
  224. Glyphs.ContinuousMeterSegment.ToString (),
  225. _slider.GetContentSize ().Width * _slider.GetContentSize ().Height));
  226. }
  227. // TODO: This is unnecessary. If Scroll.Width/Height is Dim.Auto, the Superview will get resized automatically.
  228. private void SuperView_LayoutComplete (object sender, LayoutEventArgs e)
  229. {
  230. if (!_slider._wasSliderMouse)
  231. {
  232. AdjustSlider ();
  233. }
  234. else
  235. {
  236. _slider._wasSliderMouse = false;
  237. }
  238. }
  239. }