ScrollSlider.cs 13 KB

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