ScrollSlider.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. #nullable enable
  2. using System.ComponentModel;
  3. namespace Terminal.Gui.Views;
  4. /// <summary>
  5. /// Represents the proportion of the visible content to the Viewport in a <see cref="ScrollBar"/>.
  6. /// Can be dragged with the mouse, constrained by the size of the Viewport of it's superview. Can be
  7. /// oriented either vertically or horizontally.
  8. /// </summary>
  9. public class ScrollSlider : View, IOrientation, IDesignable
  10. {
  11. /// <summary>
  12. /// Initializes a new instance.
  13. /// </summary>
  14. public ScrollSlider ()
  15. {
  16. Id = "scrollSlider";
  17. WantMousePositionReports = true;
  18. _orientationHelper = new (this); // Do not use object initializer!
  19. _orientationHelper.Orientation = Orientation.Vertical;
  20. _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
  21. _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
  22. OnOrientationChanged (Orientation);
  23. HighlightStates = ViewBase.MouseState.In;
  24. }
  25. #region IOrientation members
  26. private readonly OrientationHelper _orientationHelper;
  27. /// <inheritdoc/>
  28. public Orientation Orientation
  29. {
  30. get => _orientationHelper.Orientation;
  31. set => _orientationHelper.Orientation = value;
  32. }
  33. /// <inheritdoc/>
  34. public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
  35. /// <inheritdoc/>
  36. public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
  37. /// <inheritdoc/>
  38. public void OnOrientationChanged (Orientation newOrientation)
  39. {
  40. TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
  41. TextAlignment = Alignment.Center;
  42. VerticalTextAlignment = Alignment.Center;
  43. // Reset Position to 0 when changing orientation
  44. X = 0;
  45. Y = 0;
  46. Position = 0;
  47. // Reset opposite dim to Dim.Fill ()
  48. if (Orientation == Orientation.Vertical)
  49. {
  50. Height = Width;
  51. Width = Dim.Fill ();
  52. }
  53. else
  54. {
  55. Width = Height;
  56. Height = Dim.Fill ();
  57. }
  58. SetNeedsLayout ();
  59. }
  60. #endregion
  61. /// <inheritdoc/>
  62. protected override bool OnClearingViewport ()
  63. {
  64. if (Orientation == Orientation.Vertical)
  65. {
  66. FillRect (Viewport with { Height = Size }, Glyphs.ContinuousMeterSegment);
  67. }
  68. else
  69. {
  70. FillRect (Viewport with { Width = Size }, Glyphs.ContinuousMeterSegment);
  71. }
  72. return true;
  73. }
  74. private int? _size;
  75. /// <summary>
  76. /// Gets or sets the size of the ScrollSlider. This is a helper that gets or sets Width or Height depending
  77. /// on <see cref="Orientation"/>. The size will be clamped between 1 and the dimension of
  78. /// the <see cref="View.SuperView"/>'s Viewport.
  79. /// </summary>
  80. /// <remarks>
  81. /// <para>
  82. /// The dimension of the ScrollSlider that is perpendicular to the <see cref="Orientation"/> will be set to
  83. /// <see cref="Dim.Fill()"/>
  84. /// </para>
  85. /// </remarks>
  86. public int Size
  87. {
  88. get => _size ?? 1;
  89. set
  90. {
  91. if (value == _size)
  92. {
  93. return;
  94. }
  95. _size = Math.Clamp (value, 1, VisibleContentSize);
  96. if (Orientation == Orientation.Vertical)
  97. {
  98. Height = _size;
  99. }
  100. else
  101. {
  102. Width = _size;
  103. }
  104. SetNeedsLayout ();
  105. }
  106. }
  107. private int? _visibleContentSize;
  108. /// <summary>
  109. /// Gets or sets the size of the viewport into the content being scrolled. If not explicitly set, will be the
  110. /// greater of 1 and the dimension of the <see cref="View.SuperView"/>.
  111. /// </summary>
  112. public int VisibleContentSize
  113. {
  114. get
  115. {
  116. if (_visibleContentSize.HasValue)
  117. {
  118. return _visibleContentSize.Value;
  119. }
  120. return Math.Max (1, Orientation == Orientation.Vertical ? SuperView?.Viewport.Height ?? 2048 : SuperView?.Viewport.Width ?? 2048);
  121. }
  122. set
  123. {
  124. if (value == _visibleContentSize)
  125. {
  126. return;
  127. }
  128. _visibleContentSize = int.Max (1, value);
  129. if (_position >= _visibleContentSize - _size)
  130. {
  131. Position = _position;
  132. }
  133. SetNeedsLayout ();
  134. }
  135. }
  136. private int _position;
  137. /// <summary>
  138. /// Gets or sets the position of the ScrollSlider relative to the size of the ScrollSlider's Frame.
  139. /// The position will be constrained such that the ScrollSlider will not go outside the Viewport of
  140. /// the <see cref="View.SuperView"/>.
  141. /// </summary>
  142. public int Position
  143. {
  144. get => _position;
  145. set
  146. {
  147. int clampedPosition = ClampPosition (value);
  148. if (_position == clampedPosition)
  149. {
  150. return;
  151. }
  152. RaisePositionChangeEvents (clampedPosition);
  153. SetNeedsLayout ();
  154. }
  155. }
  156. /// <summary>
  157. /// Moves the scroll slider to the specified position. Does not clamp.
  158. /// </summary>
  159. /// <param name="position"></param>
  160. internal void MoveToPosition (int position)
  161. {
  162. if (Orientation == Orientation.Vertical)
  163. {
  164. Y = _position + SliderPadding / 2;
  165. }
  166. else
  167. {
  168. X = _position + SliderPadding / 2;
  169. }
  170. }
  171. /// <summary>
  172. /// INTERNAL API (for unit tests) - Clamps the position such that the right side of the slider
  173. /// never goes past the edge of the Viewport.
  174. /// </summary>
  175. /// <param name="newPosition"></param>
  176. /// <returns></returns>
  177. internal int ClampPosition (int newPosition)
  178. {
  179. return Math.Clamp (newPosition, 0, Math.Max (SliderPadding / 2, VisibleContentSize - SliderPadding - Size));
  180. }
  181. private void RaisePositionChangeEvents (int newPosition)
  182. {
  183. if (OnPositionChanging (_position, newPosition))
  184. {
  185. return;
  186. }
  187. CancelEventArgs<int> args = new (ref _position, ref newPosition);
  188. PositionChanging?.Invoke (this, args);
  189. if (args.Cancel)
  190. {
  191. return;
  192. }
  193. int distance = newPosition - _position;
  194. _position = ClampPosition (newPosition);
  195. MoveToPosition (_position);
  196. OnPositionChanged (_position);
  197. PositionChanged?.Invoke (this, new (in _position));
  198. OnScrolled (distance);
  199. Scrolled?.Invoke (this, new (in distance));
  200. RaiseSelecting (new CommandContext<KeyBinding> (Command.Select, this, new KeyBinding ([Command.Select], null, distance)));
  201. }
  202. /// <summary>
  203. /// Called when <see cref="Position"/> is changing. Return true to cancel the change.
  204. /// </summary>
  205. protected virtual bool OnPositionChanging (int currentPos, int newPos) { return false; }
  206. /// <summary>
  207. /// Raised when the <see cref="Position"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
  208. /// <see langword="true"/> to prevent the position from being changed.
  209. /// </summary>
  210. public event EventHandler<CancelEventArgs<int>>? PositionChanging;
  211. /// <summary>Called when <see cref="Position"/> has changed.</summary>
  212. protected virtual void OnPositionChanged (int position) { }
  213. /// <summary>Raised when the <see cref="Position"/> has changed.</summary>
  214. public event EventHandler<EventArgs<int>>? PositionChanged;
  215. /// <summary>Called when <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
  216. protected virtual void OnScrolled (int distance) { }
  217. /// <summary>Raised when the <see cref="Position"/> has changed. Indicates how much to scroll.</summary>
  218. public event EventHandler<EventArgs<int>>? Scrolled;
  219. /// <inheritdoc />
  220. protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attribute currentAttribute)
  221. {
  222. if (role == VisualRole.Normal)
  223. {
  224. currentAttribute = GetAttributeForRole (VisualRole.HotNormal);
  225. return true;
  226. }
  227. return base.OnGettingAttributeForRole (role, ref currentAttribute);
  228. }
  229. private int _lastLocation = -1;
  230. /// <summary>
  231. /// Gets or sets the amount to pad the start and end of the scroll slider. The default is 0.
  232. /// </summary>
  233. /// <remarks>
  234. /// When the scroll slider is used by <see cref="ScrollBar"/>, which has increment and decrement buttons, the
  235. /// SliderPadding should be set to the size of the buttons (typically 2).
  236. /// </remarks>
  237. public int SliderPadding { get; set; }
  238. /// <inheritdoc/>
  239. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  240. {
  241. if (SuperView is null)
  242. {
  243. return false;
  244. }
  245. if (mouseEvent.IsSingleDoubleOrTripleClicked)
  246. {
  247. return true;
  248. }
  249. int location = (Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X);
  250. int offsetFromLastLocation = _lastLocation > -1 ? location - _lastLocation : 0;
  251. int superViewDimension = VisibleContentSize;
  252. if (mouseEvent.IsPressed || mouseEvent.IsReleased)
  253. {
  254. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1)
  255. {
  256. if (Application.MouseGrabHandler.MouseGrabView != this)
  257. {
  258. Application.MouseGrabHandler.GrabMouse (this);
  259. _lastLocation = location;
  260. }
  261. }
  262. else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
  263. {
  264. int currentLocation;
  265. if (Orientation == Orientation.Vertical)
  266. {
  267. currentLocation = Frame.Y;
  268. }
  269. else
  270. {
  271. currentLocation = Frame.X;
  272. }
  273. currentLocation -= SliderPadding / 2;
  274. int newLocation = currentLocation + offsetFromLastLocation;
  275. Position = newLocation;
  276. }
  277. else if (mouseEvent.Flags == MouseFlags.Button1Released)
  278. {
  279. _lastLocation = -1;
  280. if (Application.MouseGrabHandler.MouseGrabView == this)
  281. {
  282. Application.MouseGrabHandler.UngrabMouse ();
  283. }
  284. }
  285. return true;
  286. }
  287. return false;
  288. }
  289. /// <summary>
  290. /// Gets the slider size.
  291. /// </summary>
  292. /// <param name="scrollableContentSize">The size of the content.</param>
  293. /// <param name="visibleContentSize">The size of the visible content.</param>
  294. /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
  295. public static int CalculateSize (
  296. int scrollableContentSize,
  297. int visibleContentSize,
  298. int sliderBounds
  299. )
  300. {
  301. if (scrollableContentSize <= 0 || sliderBounds <= 0)
  302. {
  303. return 1; // Slider must be at least 1
  304. }
  305. if (visibleContentSize <= 0 || scrollableContentSize <= visibleContentSize)
  306. {
  307. return sliderBounds;
  308. }
  309. double sliderSizeD = ((double)visibleContentSize / scrollableContentSize) * sliderBounds;
  310. int sliderSize = (int)Math.Floor (sliderSizeD);
  311. return Math.Clamp (sliderSize, 1, sliderBounds);
  312. }
  313. /// <summary>
  314. /// Calculates the slider position.
  315. /// </summary>
  316. /// <param name="scrollableContentSize">The size of the content.</param>
  317. /// <param name="visibleContentSize">The size of the visible content.</param>
  318. /// <param name="contentPosition">The position in the content (between 0 and <paramref name="scrollableContentSize"/>).</param>
  319. /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
  320. /// <param name="direction">The direction the slider is moving.</param>
  321. internal static int CalculatePosition (
  322. int scrollableContentSize,
  323. int visibleContentSize,
  324. int contentPosition,
  325. int sliderBounds,
  326. NavigationDirection direction
  327. )
  328. {
  329. if (scrollableContentSize - visibleContentSize <= 0 || sliderBounds <= 0)
  330. {
  331. return 0;
  332. }
  333. int calculatedSliderSize = CalculateSize (scrollableContentSize, visibleContentSize, sliderBounds);
  334. double newSliderPosition = ((double)contentPosition / (scrollableContentSize - visibleContentSize)) * (sliderBounds - calculatedSliderSize);
  335. return Math.Clamp ((int)Math.Round (newSliderPosition), 0, sliderBounds - calculatedSliderSize);
  336. }
  337. /// <summary>
  338. /// Calculates the content position.
  339. /// </summary>
  340. /// <param name="scrollableContentSize">The size of the content.</param>
  341. /// <param name="visibleContentSize">The size of the visible content.</param>
  342. /// <param name="sliderPosition">The position of the slider.</param>
  343. /// <param name="sliderBounds">The bounds of the area the slider moves in (e.g. the size of the <see cref="ScrollBar"/> minus 2).</param>
  344. internal static int CalculateContentPosition (
  345. int scrollableContentSize,
  346. int visibleContentSize,
  347. int sliderPosition,
  348. int sliderBounds
  349. )
  350. {
  351. int sliderSize = CalculateSize (scrollableContentSize, visibleContentSize, sliderBounds);
  352. double pos = ((double)(sliderPosition) / (sliderBounds - sliderSize)) * (scrollableContentSize - visibleContentSize);
  353. if (pos is double.NaN)
  354. {
  355. return 0;
  356. }
  357. double rounded = Math.Ceiling (pos);
  358. return (int)Math.Clamp (rounded, 0, Math.Max (0, scrollableContentSize - sliderSize));
  359. }
  360. /// <inheritdoc/>
  361. public bool EnableForDesign ()
  362. {
  363. Size = 5;
  364. return true;
  365. }
  366. }