Scroll.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. #nullable enable
  2. using System.ComponentModel;
  3. namespace Terminal.Gui;
  4. /// <summary>
  5. /// Indicates the size of scrollable content and provides a visible element, referred to as the "ScrollSlider" that
  6. /// that is sized to
  7. /// show the proportion of the scrollable content to the size of the <see cref="View.Viewport"/>. The ScrollSlider
  8. /// can be dragged with the mouse. A Scroll can be oriented either vertically or horizontally and is used within a
  9. /// <see cref="ScrollBar"/>.
  10. /// </summary>
  11. /// <remarks>
  12. /// <para>
  13. /// By default, this view cannot be focused and does not support keyboard.
  14. /// </para>
  15. /// </remarks>
  16. public class Scroll : View, IOrientation, IDesignable
  17. {
  18. internal readonly ScrollSlider _slider;
  19. /// <inheritdoc/>
  20. public Scroll ()
  21. {
  22. _slider = new ();
  23. Add (_slider);
  24. _slider.FrameChanged += OnSliderOnFrameChanged;
  25. CanFocus = false;
  26. _orientationHelper = new (this); // Do not use object initializer!
  27. _orientationHelper.Orientation = Orientation.Vertical;
  28. _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
  29. _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
  30. // This sets the width/height etc...
  31. OnOrientationChanged (Orientation);
  32. }
  33. /// <inheritdoc/>
  34. protected override void OnSubviewLayout (LayoutEventArgs args)
  35. {
  36. if (ViewportDimension < 1)
  37. {
  38. _slider.Size = 1;
  39. return;
  40. }
  41. _slider.Size = (int)Math.Clamp (Math.Floor ((double)ViewportDimension * ViewportDimension / (Size)), 1, ViewportDimension);
  42. }
  43. #region IOrientation members
  44. private readonly OrientationHelper _orientationHelper;
  45. /// <inheritdoc/>
  46. public Orientation Orientation
  47. {
  48. get => _orientationHelper.Orientation;
  49. set => _orientationHelper.Orientation = value;
  50. }
  51. /// <inheritdoc/>
  52. public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
  53. /// <inheritdoc/>
  54. public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
  55. /// <inheritdoc/>
  56. public void OnOrientationChanged (Orientation newOrientation)
  57. {
  58. TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
  59. TextAlignment = Alignment.Center;
  60. VerticalTextAlignment = Alignment.Center;
  61. X = 0;
  62. Y = 0;
  63. if (Orientation == Orientation.Vertical)
  64. {
  65. Width = 1;
  66. Height = Dim.Fill ();
  67. }
  68. else
  69. {
  70. Width = Dim.Fill ();
  71. Height = 1;
  72. }
  73. _slider.Orientation = newOrientation;
  74. }
  75. #endregion
  76. /// <summary>
  77. /// Gets or sets whether the Scroll will show the percentage the slider
  78. /// takes up within the <see cref="Size"/>.
  79. /// </summary>
  80. public bool ShowPercent
  81. {
  82. get => _slider.ShowPercent;
  83. set => _slider.ShowPercent = value;
  84. }
  85. private int ViewportDimension => Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
  86. private int _size;
  87. /// <summary>
  88. /// Gets or sets the total size of the content that can be scrolled.
  89. /// </summary>
  90. public int Size
  91. {
  92. get => _size;
  93. set
  94. {
  95. if (value == _size || value < 0)
  96. {
  97. return;
  98. }
  99. _size = value;
  100. OnSizeChanged (_size);
  101. SizeChanged?.Invoke (this, new (in _size));
  102. SetNeedsLayout ();
  103. }
  104. }
  105. /// <summary>Called when <see cref="Size"/> has changed. </summary>
  106. protected virtual void OnSizeChanged (int size) { }
  107. /// <summary>Raised when <see cref="Size"/> has changed.</summary>
  108. public event EventHandler<EventArgs<int>>? SizeChanged;
  109. #region SliderPosition
  110. private void OnSliderOnFrameChanged (object? sender, EventArgs<Rectangle> args)
  111. {
  112. if (ViewportDimension == 0)
  113. {
  114. return;
  115. }
  116. int framePos = Orientation == Orientation.Vertical ? args.CurrentValue.Y : args.CurrentValue.X;
  117. SliderPosition = framePos;
  118. }
  119. /// <summary>
  120. /// Gets or sets the position of the start of the Scroll slider, within the Viewport.
  121. /// </summary>
  122. public int SliderPosition
  123. {
  124. get => CalculateSliderPosition (_contentPosition);
  125. set => RaiseSliderPositionChangeEvents (value);
  126. }
  127. private void RaiseSliderPositionChangeEvents (int newSliderPosition)
  128. {
  129. int currentSliderPosition = CalculateSliderPosition (_contentPosition);
  130. if (newSliderPosition > Size - ViewportDimension || currentSliderPosition == newSliderPosition)
  131. {
  132. return;
  133. }
  134. if (OnSliderPositionChanging (currentSliderPosition, newSliderPosition))
  135. {
  136. return;
  137. }
  138. CancelEventArgs<int> args = new (ref currentSliderPosition, ref newSliderPosition);
  139. SliderPositionChanging?.Invoke (this, args);
  140. if (args.Cancel)
  141. {
  142. return;
  143. }
  144. // This sets the slider position and clamps the value
  145. _slider.Position = newSliderPosition;
  146. ContentPosition = (int)Math.Round ((double)newSliderPosition / (ViewportDimension - _slider.Size) * (Size - ViewportDimension));
  147. OnSliderPositionChanged (newSliderPosition);
  148. SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
  149. }
  150. /// <summary>
  151. /// Called when <see cref="SliderPosition"/> is changing. Return true to cancel the change.
  152. /// </summary>
  153. protected virtual bool OnSliderPositionChanging (int currentSliderPosition, int newSliderPosition) { return false; }
  154. /// <summary>
  155. /// Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
  156. /// <see langword="true"/> to prevent the position from being changed.
  157. /// </summary>
  158. public event EventHandler<CancelEventArgs<int>>? SliderPositionChanging;
  159. /// <summary>Called when <see cref="SliderPosition"/> has changed.</summary>
  160. protected virtual void OnSliderPositionChanged (int position) { }
  161. /// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
  162. public event EventHandler<EventArgs<int>>? SliderPositionChanged;
  163. private int CalculateSliderPosition (int contentPosition)
  164. {
  165. if (Size - ViewportDimension == 0)
  166. {
  167. return 0;
  168. }
  169. return (int)Math.Round ((double)contentPosition / (Size - ViewportDimension) * (ViewportDimension - _slider.Size));
  170. }
  171. #endregion SliderPosition
  172. #region ContentPosition
  173. private int _contentPosition;
  174. /// <summary>
  175. /// Gets or sets the position of the ScrollSlider within the range of 0...<see cref="Size"/>.
  176. /// </summary>
  177. public int ContentPosition
  178. {
  179. get => _contentPosition;
  180. set
  181. {
  182. if (value == _contentPosition)
  183. {
  184. return;
  185. }
  186. RaiseContentPositionChangeEvents (value);
  187. }
  188. }
  189. private void RaiseContentPositionChangeEvents (int newContentPosition)
  190. {
  191. // Clamp the value between 0 and Size - ViewportDimension
  192. newContentPosition = (int)Math.Clamp (newContentPosition, 0, Math.Max (0, Size - ViewportDimension));
  193. if (OnContentPositionChanging (_contentPosition, newContentPosition))
  194. {
  195. return;
  196. }
  197. CancelEventArgs<int> args = new (ref _contentPosition, ref newContentPosition);
  198. ContentPositionChanging?.Invoke (this, args);
  199. if (args.Cancel)
  200. {
  201. return;
  202. }
  203. _contentPosition = newContentPosition;
  204. SliderPosition = CalculateSliderPosition (_contentPosition);
  205. OnContentPositionChanged (_contentPosition);
  206. ContentPositionChanged?.Invoke (this, new (in _contentPosition));
  207. }
  208. /// <summary>
  209. /// Called when <see cref="ContentPosition"/> is changing. Return true to cancel the change.
  210. /// </summary>
  211. protected virtual bool OnContentPositionChanging (int currentPos, int newPos) { return false; }
  212. /// <summary>
  213. /// Raised when the <see cref="ContentPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
  214. /// <see langword="true"/> to prevent the position from being changed.
  215. /// </summary>
  216. public event EventHandler<CancelEventArgs<int>>? ContentPositionChanging;
  217. /// <summary>Called when <see cref="ContentPosition"/> has changed.</summary>
  218. protected virtual void OnContentPositionChanged (int position) { }
  219. /// <summary>Raised when the <see cref="ContentPosition"/> has changed.</summary>
  220. public event EventHandler<EventArgs<int>>? ContentPositionChanged;
  221. #endregion ContentPosition
  222. /// <inheritdoc/>
  223. protected override bool OnClearingViewport ()
  224. {
  225. FillRect (Viewport, Glyphs.Stipple);
  226. return true;
  227. }
  228. /// <inheritdoc/>
  229. protected override bool OnMouseClick (MouseEventArgs args)
  230. {
  231. if (!args.IsSingleClicked)
  232. {
  233. return false;
  234. }
  235. if (Orientation == Orientation.Vertical)
  236. {
  237. // If the position is w/in the slider frame ignore
  238. if (args.Position.Y >= _slider.Frame.Y && args.Position.Y < _slider.Frame.Y + _slider.Frame.Height)
  239. {
  240. return false;
  241. }
  242. SliderPosition = args.Position.Y;
  243. }
  244. else
  245. {
  246. // If the position is w/in the slider frame ignore
  247. if (args.Position.X >= _slider.Frame.X && args.Position.X < _slider.Frame.X + _slider.Frame.Width)
  248. {
  249. return false;
  250. }
  251. SliderPosition = args.Position.X;
  252. }
  253. return true;
  254. }
  255. /// <summary>
  256. /// Gets or sets the amount each mouse hweel event will incremenet/decrement the <see cref="ContentPosition"/>.
  257. /// </summary>
  258. /// <remarks>
  259. /// The default is 1.
  260. /// </remarks>
  261. public int Increment { get; set; } = 1;
  262. /// <inheritdoc/>
  263. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  264. {
  265. if (SuperView is null)
  266. {
  267. return false;
  268. }
  269. if (!mouseEvent.IsWheel)
  270. {
  271. return false;
  272. }
  273. if (Orientation == Orientation.Vertical)
  274. {
  275. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown))
  276. {
  277. ContentPosition += Increment;
  278. }
  279. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp))
  280. {
  281. ContentPosition -= Increment;
  282. }
  283. }
  284. else
  285. {
  286. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight))
  287. {
  288. ContentPosition += Increment;
  289. }
  290. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft))
  291. {
  292. ContentPosition -= Increment;
  293. }
  294. }
  295. return true;
  296. }
  297. /// <inheritdoc/>
  298. public bool EnableForDesign ()
  299. {
  300. OrientationChanged += (sender, args) =>
  301. {
  302. if (args.CurrentValue == Orientation.Vertical)
  303. {
  304. Width = 1;
  305. Height = Dim.Fill ();
  306. }
  307. else
  308. {
  309. Width = Dim.Fill ();
  310. Height = 1;
  311. }
  312. };
  313. Width = 1;
  314. Height = Dim.Fill ();
  315. Size = 1000;
  316. ContentPosition = 10;
  317. return true;
  318. }
  319. }