ScrollSlider.cs 16 KB

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