Scroll.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. #nullable enable
  2. using System.ComponentModel;
  3. using System.Drawing;
  4. namespace Terminal.Gui;
  5. /// <summary>
  6. /// Indicates the size of scrollable content and provides a visible element, referred to as the "ScrollSlider" that
  7. /// that is sized to
  8. /// show the proportion of the scrollable content to the size of the <see cref="View.Viewport"/>. The ScrollSlider
  9. /// can be dragged with the mouse. A Scroll can be oriented either vertically or horizontally and is used within a
  10. /// <see cref="ScrollBar"/>.
  11. /// </summary>
  12. /// <remarks>
  13. /// <para>
  14. /// By default, this view cannot be focused and does not support keyboard.
  15. /// </para>
  16. /// </remarks>
  17. public class Scroll : View, IOrientation, IDesignable
  18. {
  19. internal readonly ScrollSlider _slider;
  20. /// <inheritdoc/>
  21. public Scroll ()
  22. {
  23. _slider = new ();
  24. Add (_slider);
  25. _slider.FrameChanged += OnSliderOnFrameChanged;
  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 => Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
  87. private int _size;
  88. /// <summary>
  89. /// Gets or sets the total size of the content that can be scrolled.
  90. /// </summary>
  91. public int Size
  92. {
  93. get => _size;
  94. set
  95. {
  96. if (value == _size || value < 0)
  97. {
  98. return;
  99. }
  100. _size = value;
  101. OnSizeChanged (_size);
  102. SizeChanged?.Invoke (this, new (in _size));
  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. private int _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. RaisePositionChangeEvents (_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 => _sliderPosition;
  125. set => RaisePositionChangeEvents (_sliderPosition, value);
  126. }
  127. /// <summary>
  128. /// Gets or sets the position of the ScrollSlider within the range of 0...<see cref="Size"/>.
  129. /// </summary>
  130. public int ContentPosition
  131. {
  132. get
  133. {
  134. if (ViewportDimension - _slider.Size == 0)
  135. {
  136. return 0;
  137. }
  138. return (int)Math.Round (GetCurrentContentPosition ());
  139. }
  140. set
  141. {
  142. if (Size - ViewportDimension == 0)
  143. {
  144. SliderPosition = 0;
  145. return;
  146. }
  147. double newContentPos = (double)value / (ViewportDimension - _slider.Size) * (Size - ViewportDimension);
  148. double newSliderPos = (double)value / (Size - ViewportDimension) * (ViewportDimension - _slider.Size);
  149. int newSliderPosition = (int)Math.Ceiling (newSliderPos);
  150. if (newContentPos >= GetCurrentContentPosition ())
  151. {
  152. newSliderPosition = (int)Math.Floor (newSliderPos);
  153. }
  154. if (SliderPosition != newSliderPosition)
  155. {
  156. SliderPosition = newSliderPosition;
  157. }
  158. }
  159. }
  160. private double GetCurrentContentPosition ()
  161. {
  162. return (double)SliderPosition / (ViewportDimension - _slider.Size) * (Size - ViewportDimension);
  163. }
  164. private void RaisePositionChangeEvents (int currentSliderPosition, int newSliderPosition)
  165. {
  166. if (OnPositionChanging (currentSliderPosition, newSliderPosition))
  167. {
  168. _slider.Position = currentSliderPosition;
  169. return;
  170. }
  171. CancelEventArgs<int> args = new (ref currentSliderPosition, ref newSliderPosition);
  172. SliderPositionChanging?.Invoke (this, args);
  173. if (args.Cancel)
  174. {
  175. _slider.Position = currentSliderPosition;
  176. return;
  177. }
  178. // This sets the slider position and clamps the value
  179. _slider.Position = newSliderPosition;
  180. if (_slider.Position == _sliderPosition)
  181. {
  182. return;
  183. }
  184. _sliderPosition = newSliderPosition;
  185. OnPositionChanged (_sliderPosition);
  186. SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
  187. }
  188. /// <summary>
  189. /// Called when <see cref="SliderPosition"/> is changing. Return true to cancel the change.
  190. /// </summary>
  191. protected virtual bool OnPositionChanging (int currentPos, int newPos)
  192. {
  193. return false;
  194. }
  195. /// <summary>
  196. /// Raised when the <see cref="SliderPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
  197. /// <see langword="true"/> to prevent the position from being changed.
  198. /// </summary>
  199. public event EventHandler<CancelEventArgs<int>>? SliderPositionChanging;
  200. /// <summary>Called when <see cref="SliderPosition"/> has changed.</summary>
  201. protected virtual void OnPositionChanged (int position) { }
  202. /// <summary>Raised when the <see cref="SliderPosition"/> has changed.</summary>
  203. public event EventHandler<EventArgs<int>>? SliderPositionChanged;
  204. /// <inheritdoc/>
  205. protected override bool OnClearingViewport ()
  206. {
  207. FillRect (Viewport, Glyphs.Stipple);
  208. return true;
  209. }
  210. /// <inheritdoc />
  211. protected override bool OnMouseClick (MouseEventArgs args)
  212. {
  213. if (!args.IsSingleClicked)
  214. {
  215. return false;
  216. }
  217. if (Orientation == Orientation.Vertical)
  218. {
  219. // If the position is w/in the slider frame ignore
  220. if (args.Position.Y >= _slider.Frame.Y && args.Position.Y < _slider.Frame.Y + _slider.Frame.Height)
  221. {
  222. return false;
  223. }
  224. SliderPosition = args.Position.Y;
  225. }
  226. else
  227. {
  228. // If the position is w/in the slider frame ignore
  229. if (args.Position.X >= _slider.Frame.X && args.Position.X < _slider.Frame.X + _slider.Frame.Width)
  230. {
  231. return false;
  232. }
  233. SliderPosition = args.Position.X;
  234. }
  235. return true;
  236. }
  237. /// <inheritdoc/>
  238. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  239. {
  240. if (SuperView is null)
  241. {
  242. return false;
  243. }
  244. if (!mouseEvent.IsWheel)
  245. {
  246. return false;
  247. }
  248. if (Orientation == Orientation.Vertical)
  249. {
  250. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown))
  251. {
  252. SliderPosition++;
  253. }
  254. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp))
  255. {
  256. SliderPosition--;
  257. }
  258. }
  259. else
  260. {
  261. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight))
  262. {
  263. SliderPosition++;
  264. }
  265. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft))
  266. {
  267. SliderPosition--;
  268. }
  269. }
  270. return true;
  271. }
  272. /// <inheritdoc />
  273. public bool EnableForDesign ()
  274. {
  275. OrientationChanged += (sender, args) =>
  276. {
  277. if (args.CurrentValue == Orientation.Vertical)
  278. {
  279. Width = 1;
  280. Height = Dim.Fill ();
  281. }
  282. else
  283. {
  284. Width = Dim.Fill ();
  285. Height = 1;
  286. }
  287. };
  288. Width = 1;
  289. Height = Dim.Fill ();
  290. Size = 1000;
  291. SliderPosition = 10;
  292. return true;
  293. }
  294. }