ScrollSlider.cs 14 KB

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