Scroll.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  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. base.Add (_slider);
  24. _slider.Scroll += SliderOnScroll;
  25. _slider.PositionChanged += SliderOnPositionChanged;
  26. CanFocus = false;
  27. _orientationHelper = new (this); // Do not use object initializer!
  28. _orientationHelper.Orientation = Orientation.Vertical;
  29. _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
  30. _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
  31. // This sets the width/height etc...
  32. OnOrientationChanged (Orientation);
  33. }
  34. /// <inheritdoc/>
  35. protected override void OnSubviewLayout (LayoutEventArgs args)
  36. {
  37. if (ViewportDimension < 1)
  38. {
  39. _slider.Size = 1;
  40. return;
  41. }
  42. _slider.Size = (int)Math.Clamp (Math.Floor ((double)ViewportDimension * ViewportDimension / (Size)), 1, ViewportDimension);
  43. }
  44. #region IOrientation members
  45. private readonly OrientationHelper _orientationHelper;
  46. /// <inheritdoc/>
  47. public Orientation Orientation
  48. {
  49. get => _orientationHelper.Orientation;
  50. set => _orientationHelper.Orientation = value;
  51. }
  52. /// <inheritdoc/>
  53. public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
  54. /// <inheritdoc/>
  55. public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
  56. /// <inheritdoc/>
  57. public void OnOrientationChanged (Orientation newOrientation)
  58. {
  59. TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
  60. TextAlignment = Alignment.Center;
  61. VerticalTextAlignment = Alignment.Center;
  62. X = 0;
  63. Y = 0;
  64. if (Orientation == Orientation.Vertical)
  65. {
  66. Width = 1;
  67. Height = Dim.Fill ();
  68. }
  69. else
  70. {
  71. Width = Dim.Fill ();
  72. Height = 1;
  73. }
  74. _slider.Orientation = newOrientation;
  75. }
  76. #endregion
  77. /// <summary>
  78. /// Gets or sets whether the Scroll will show the percentage the slider
  79. /// takes up within the <see cref="Size"/>.
  80. /// </summary>
  81. public bool ShowPercent
  82. {
  83. get => _slider.ShowPercent;
  84. set => _slider.ShowPercent = value;
  85. }
  86. private int? _viewportDimension;
  87. /// <summary>
  88. /// Gets or sets the size of the viewport into the content being scrolled, bounded by <see cref="Size"/>.
  89. /// </summary>
  90. /// <remarks>
  91. /// If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
  92. /// </remarks>
  93. public int ViewportDimension
  94. {
  95. get
  96. {
  97. if (_viewportDimension.HasValue)
  98. {
  99. return _viewportDimension.Value;
  100. }
  101. return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
  102. }
  103. set => _viewportDimension = value;
  104. }
  105. private int _size;
  106. /// <summary>
  107. /// Gets or sets the size of the content that can be scrolled.
  108. /// </summary>
  109. public int Size
  110. {
  111. get => _size;
  112. set
  113. {
  114. if (value == _size || value < 0)
  115. {
  116. return;
  117. }
  118. _size = value;
  119. OnSizeChanged (_size);
  120. SizeChanged?.Invoke (this, new (in _size));
  121. SetNeedsLayout ();
  122. }
  123. }
  124. /// <summary>Called when <see cref="Size"/> has changed. </summary>
  125. protected virtual void OnSizeChanged (int size) { }
  126. /// <summary>Raised when <see cref="Size"/> has changed.</summary>
  127. public event EventHandler<EventArgs<int>>? SizeChanged;
  128. #region SliderPosition
  129. private void SliderOnPositionChanged (object? sender, EventArgs<int> e)
  130. {
  131. if (ViewportDimension == 0)
  132. {
  133. return;
  134. }
  135. int calculatedSliderPos = CalculateSliderPosition (_contentPosition);
  136. ContentPosition = (int)Math.Round ((double)e.CurrentValue / (ViewportDimension - _slider.Size) * (Size - ViewportDimension));
  137. RaiseSliderPositionChangeEvents (calculatedSliderPos, e.CurrentValue);
  138. }
  139. private void SliderOnScroll (object? sender, EventArgs<int> e)
  140. {
  141. if (ViewportDimension == 0)
  142. {
  143. return;
  144. }
  145. }
  146. /// <summary>
  147. /// Gets or sets the position of the start of the Scroll slider, within the Viewport.
  148. /// </summary>
  149. public int GetSliderPosition () => CalculateSliderPosition (_contentPosition);
  150. private void RaiseSliderPositionChangeEvents (int calculatedSliderPosition, int newSliderPosition)
  151. {
  152. if (/*newSliderPosition > Size - ViewportDimension ||*/ calculatedSliderPosition == newSliderPosition)
  153. {
  154. return;
  155. }
  156. // This sets the slider position and clamps the value
  157. _slider.Position = newSliderPosition;
  158. OnSliderPositionChanged (newSliderPosition);
  159. SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
  160. }
  161. /// <summary>Called when the slider position has changed.</summary>
  162. protected virtual void OnSliderPositionChanged (int position) { }
  163. /// <summary>Raised when the slider position has changed.</summary>
  164. public event EventHandler<EventArgs<int>>? SliderPositionChanged;
  165. private int CalculateSliderPosition (int contentPosition)
  166. {
  167. if (Size - ViewportDimension == 0)
  168. {
  169. return 0;
  170. }
  171. return (int)Math.Round ((double)contentPosition / (Size - ViewportDimension) * (ViewportDimension - _slider.Size));
  172. }
  173. #endregion SliderPosition
  174. #region ContentPosition
  175. private int _contentPosition;
  176. /// <summary>
  177. /// Gets or sets the position of the slider relative to <see cref="Size"/>.
  178. /// </summary>
  179. /// <remarks>
  180. /// <para>
  181. /// The content position is clamped to 0 and <see cref="Size"/> minus <see cref="ViewportDimension"/>.
  182. /// </para>
  183. /// <para>
  184. /// Setting will result in the <see cref="ContentPositionChanging"/> and <see cref="ContentPositionChanged"/>
  185. /// events being raised.
  186. /// </para>
  187. /// </remarks>
  188. public int ContentPosition
  189. {
  190. get => _contentPosition;
  191. set
  192. {
  193. if (value == _contentPosition)
  194. {
  195. return;
  196. }
  197. // Clamp the value between 0 and Size - ViewportDimension
  198. int newContentPosition = (int)Math.Clamp (value, 0, Math.Max (0, Size - ViewportDimension));
  199. RaiseContentPositionChangeEvents (newContentPosition);
  200. _slider.SetPosition (CalculateSliderPosition (_contentPosition));
  201. }
  202. }
  203. private void RaiseContentPositionChangeEvents (int newContentPosition)
  204. {
  205. if (OnContentPositionChanging (_contentPosition, newContentPosition))
  206. {
  207. return;
  208. }
  209. CancelEventArgs<int> args = new (ref _contentPosition, ref newContentPosition);
  210. ContentPositionChanging?.Invoke (this, args);
  211. if (args.Cancel)
  212. {
  213. return;
  214. }
  215. _contentPosition = newContentPosition;
  216. OnContentPositionChanged (_contentPosition);
  217. ContentPositionChanged?.Invoke (this, new (in _contentPosition));
  218. }
  219. /// <summary>
  220. /// Called when <see cref="ContentPosition"/> is changing. Return true to cancel the change.
  221. /// </summary>
  222. protected virtual bool OnContentPositionChanging (int currentPos, int newPos) { return false; }
  223. /// <summary>
  224. /// Raised when the <see cref="ContentPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
  225. /// <see langword="true"/> to prevent the position from being changed.
  226. /// </summary>
  227. public event EventHandler<CancelEventArgs<int>>? ContentPositionChanging;
  228. /// <summary>Called when <see cref="ContentPosition"/> has changed.</summary>
  229. protected virtual void OnContentPositionChanged (int position) { }
  230. /// <summary>Raised when the <see cref="ContentPosition"/> has changed.</summary>
  231. public event EventHandler<EventArgs<int>>? ContentPositionChanged;
  232. #endregion ContentPosition
  233. /// <inheritdoc/>
  234. protected override bool OnClearingViewport ()
  235. {
  236. FillRect (Viewport, Glyphs.Stipple);
  237. return true;
  238. }
  239. /// <inheritdoc/>
  240. protected override bool OnMouseClick (MouseEventArgs args)
  241. {
  242. // Check if the mouse click is a single click
  243. if (!args.IsSingleClicked)
  244. {
  245. return false;
  246. }
  247. int sliderCenter;
  248. int distanceFromCenter;
  249. if (Orientation == Orientation.Vertical)
  250. {
  251. sliderCenter = _slider.Frame.Y + _slider.Frame.Height / 2;
  252. distanceFromCenter = args.Position.Y - sliderCenter;
  253. }
  254. else
  255. {
  256. sliderCenter = _slider.Frame.X + _slider.Frame.Width / 2;
  257. distanceFromCenter = args.Position.X - sliderCenter;
  258. }
  259. // Ratio of the distance to the viewport dimension
  260. double ratio = (double)Math.Abs (distanceFromCenter) / ViewportDimension;
  261. // Jump size based on the ratio and the total content size
  262. int jump = (int)Math.Ceiling (ratio * Size);
  263. // Adjust the content position based on the distance
  264. ContentPosition += distanceFromCenter < 0 ? -jump : jump;
  265. return true;
  266. }
  267. /// <summary>
  268. /// Gets or sets the amount each mouse hweel event will incremenet/decrement the <see cref="ContentPosition"/>.
  269. /// </summary>
  270. /// <remarks>
  271. /// The default is 1.
  272. /// </remarks>
  273. public int Increment { get; set; } = 1;
  274. /// <inheritdoc/>
  275. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  276. {
  277. if (SuperView is null)
  278. {
  279. return false;
  280. }
  281. if (!mouseEvent.IsWheel)
  282. {
  283. return false;
  284. }
  285. if (Orientation == Orientation.Vertical)
  286. {
  287. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown))
  288. {
  289. ContentPosition += Increment;
  290. }
  291. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp))
  292. {
  293. ContentPosition -= Increment;
  294. }
  295. }
  296. else
  297. {
  298. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight))
  299. {
  300. ContentPosition += Increment;
  301. }
  302. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft))
  303. {
  304. ContentPosition -= Increment;
  305. }
  306. }
  307. return true;
  308. }
  309. /// <inheritdoc/>
  310. public bool EnableForDesign ()
  311. {
  312. OrientationChanged += (sender, args) =>
  313. {
  314. if (args.CurrentValue == Orientation.Vertical)
  315. {
  316. Width = 1;
  317. Height = Dim.Fill ();
  318. }
  319. else
  320. {
  321. Width = Dim.Fill ();
  322. Height = 1;
  323. }
  324. };
  325. Width = 1;
  326. Height = Dim.Fill ();
  327. Size = 250;
  328. return true;
  329. }
  330. }