ScrollBar.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  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 ScrollBar : View, IOrientation, IDesignable
  18. {
  19. private readonly Button _decreaseButton;
  20. internal readonly ScrollSlider _slider;
  21. private readonly Button _increaseButton;
  22. /// <inheritdoc/>
  23. public ScrollBar ()
  24. {
  25. _decreaseButton = new ()
  26. {
  27. CanFocus = false,
  28. NoDecorations = true,
  29. NoPadding = true,
  30. ShadowStyle = ShadowStyle.None,
  31. WantContinuousButtonPressed = true
  32. };
  33. _decreaseButton.Accepting += OnDecreaseButtonOnAccept;
  34. _slider = new ()
  35. {
  36. ShrinkBy = 2, // For the buttons
  37. };
  38. _slider.Scrolled += SliderOnScroll;
  39. _slider.PositionChanged += SliderOnPositionChanged;
  40. _increaseButton = new ()
  41. {
  42. CanFocus = false,
  43. NoDecorations = true,
  44. NoPadding = true,
  45. ShadowStyle = ShadowStyle.None,
  46. WantContinuousButtonPressed = true
  47. };
  48. _increaseButton.Accepting += OnIncreaseButtonOnAccept;
  49. base.Add (_decreaseButton, _slider, _increaseButton);
  50. CanFocus = false;
  51. _orientationHelper = new (this); // Do not use object initializer!
  52. _orientationHelper.Orientation = Orientation.Vertical;
  53. _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e);
  54. _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e);
  55. // This sets the width/height etc...
  56. OnOrientationChanged (Orientation);
  57. void OnDecreaseButtonOnAccept (object? s, CommandEventArgs e)
  58. {
  59. ContentPosition -= Increment;
  60. e.Cancel = true;
  61. }
  62. void OnIncreaseButtonOnAccept (object? s, CommandEventArgs e)
  63. {
  64. ContentPosition += Increment;
  65. e.Cancel = true;
  66. }
  67. }
  68. /// <inheritdoc/>
  69. protected override void OnFrameChanged (in Rectangle frame)
  70. {
  71. ShowHide ();
  72. }
  73. private void ShowHide ()
  74. {
  75. if (!AutoHide || !IsInitialized)
  76. {
  77. return;
  78. }
  79. if (Orientation == Orientation.Vertical)
  80. {
  81. Visible = Frame.Height < Size;
  82. }
  83. else
  84. {
  85. Visible = Frame.Width < Size;
  86. }
  87. }
  88. /// <inheritdoc/>
  89. protected override void OnSubviewLayout (LayoutEventArgs args)
  90. {
  91. _slider.Size = CalculateSliderSize ();
  92. if (Orientation == Orientation.Vertical)
  93. {
  94. _slider.ViewportDimension = Viewport.Height - _slider.ShrinkBy;
  95. }
  96. else
  97. {
  98. _slider.ViewportDimension = Viewport.Width - _slider.ShrinkBy;
  99. }
  100. }
  101. private int CalculateSliderSize ()
  102. {
  103. if (Size == 0 || ViewportDimension == 0)
  104. {
  105. return 1;
  106. }
  107. int viewport = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
  108. return (int)Math.Clamp (Math.Floor ((double)ViewportDimension / Size * (viewport - 2)), 1, ViewportDimension);
  109. }
  110. private void PositionSubviews ()
  111. {
  112. if (Orientation == Orientation.Vertical)
  113. {
  114. _decreaseButton.Y = 0;
  115. _decreaseButton.X = 0;
  116. _decreaseButton.Width = Dim.Fill ();
  117. _decreaseButton.Height = 1;
  118. _decreaseButton.Title = Glyphs.UpArrow.ToString ();
  119. _slider.X = 0;
  120. _slider.Y = 1;
  121. _slider.Width = Dim.Fill ();
  122. _increaseButton.Y = Pos.AnchorEnd ();
  123. _increaseButton.X = 0;
  124. _increaseButton.Width = Dim.Fill ();
  125. _increaseButton.Height = 1;
  126. _increaseButton.Title = Glyphs.DownArrow.ToString ();
  127. }
  128. else
  129. {
  130. _decreaseButton.Y = 0;
  131. _decreaseButton.X = 0;
  132. _decreaseButton.Width = 1;
  133. _decreaseButton.Height = Dim.Fill ();
  134. _decreaseButton.Title = Glyphs.LeftArrow.ToString ();
  135. _slider.Y = 0;
  136. _slider.X = 1;
  137. _slider.Height = Dim.Fill ();
  138. _increaseButton.Y = 0;
  139. _increaseButton.X = Pos.AnchorEnd ();
  140. _increaseButton.Width = 1;
  141. _increaseButton.Height = Dim.Fill ();
  142. _increaseButton.Title = Glyphs.RightArrow.ToString ();
  143. }
  144. }
  145. #region IOrientation members
  146. private readonly OrientationHelper _orientationHelper;
  147. /// <inheritdoc/>
  148. public Orientation Orientation
  149. {
  150. get => _orientationHelper.Orientation;
  151. set => _orientationHelper.Orientation = value;
  152. }
  153. /// <inheritdoc/>
  154. public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
  155. /// <inheritdoc/>
  156. public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
  157. /// <inheritdoc/>
  158. public void OnOrientationChanged (Orientation newOrientation)
  159. {
  160. TextDirection = Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
  161. TextAlignment = Alignment.Center;
  162. VerticalTextAlignment = Alignment.Center;
  163. X = 0;
  164. Y = 0;
  165. if (Orientation == Orientation.Vertical)
  166. {
  167. Width = 1;
  168. Height = Dim.Fill ();
  169. }
  170. else
  171. {
  172. Width = Dim.Fill ();
  173. Height = 1;
  174. }
  175. _slider.Orientation = newOrientation;
  176. PositionSubviews ();
  177. }
  178. #endregion
  179. private bool _autoHide = true;
  180. /// <summary>
  181. /// Gets or sets whether <see cref="View.Visible"/> will be set to <see langword="false"/> if the dimension of the
  182. /// scroll bar is greater than or equal to <see cref="Size"/>.
  183. /// </summary>
  184. public bool AutoHide
  185. {
  186. get => _autoHide;
  187. set
  188. {
  189. if (_autoHide != value)
  190. {
  191. _autoHide = value;
  192. if (!AutoHide)
  193. {
  194. Visible = true;
  195. }
  196. SetNeedsLayout ();
  197. }
  198. }
  199. }
  200. public bool KeepContentInAllViewport
  201. {
  202. //get => _scroll.KeepContentInAllViewport;
  203. //set => _scroll.KeepContentInAllViewport = value;
  204. get;
  205. set;
  206. }
  207. /// <summary>
  208. /// Gets or sets whether the Scroll will show the percentage the slider
  209. /// takes up within the <see cref="Size"/>.
  210. /// </summary>
  211. public bool ShowPercent
  212. {
  213. get => _slider.ShowPercent;
  214. set => _slider.ShowPercent = value;
  215. }
  216. private int? _viewportDimension;
  217. /// <summary>
  218. /// Gets or sets the size of the viewport into the content being scrolled, bounded by <see cref="Size"/>.
  219. /// </summary>
  220. /// <remarks>
  221. /// If not explicitly set, will be the appropriate dimension of the Scroll's Frame.
  222. /// </remarks>
  223. public int ViewportDimension
  224. {
  225. get
  226. {
  227. if (_viewportDimension.HasValue)
  228. {
  229. return _viewportDimension.Value;
  230. }
  231. return Orientation == Orientation.Vertical ? Frame.Height : Frame.Width;
  232. }
  233. set => _viewportDimension = value;
  234. }
  235. private int _size;
  236. /// <summary>
  237. /// Gets or sets the size of the content that can be scrolled.
  238. /// </summary>
  239. public int Size
  240. {
  241. get => _size;
  242. set
  243. {
  244. if (value == _size || value < 0)
  245. {
  246. return;
  247. }
  248. _size = value;
  249. OnSizeChanged (_size);
  250. SizeChanged?.Invoke (this, new (in _size));
  251. SetNeedsLayout ();
  252. }
  253. }
  254. /// <summary>Called when <see cref="Size"/> has changed. </summary>
  255. protected virtual void OnSizeChanged (int size) { }
  256. /// <summary>Raised when <see cref="Size"/> has changed.</summary>
  257. public event EventHandler<EventArgs<int>>? SizeChanged;
  258. #region SliderPosition
  259. private void SliderOnPositionChanged (object? sender, EventArgs<int> e)
  260. {
  261. if (ViewportDimension == 0)
  262. {
  263. return;
  264. }
  265. int pos = e.CurrentValue;
  266. RaiseSliderPositionChangeEvents (_slider.Position, pos);
  267. }
  268. private void SliderOnScroll (object? sender, EventArgs<int> e)
  269. {
  270. if (ViewportDimension == 0)
  271. {
  272. return;
  273. }
  274. int calculatedSliderPos = CalculateSliderPosition (_contentPosition, e.CurrentValue >= 0 ? NavigationDirection.Forward : NavigationDirection.Backward);
  275. int sliderScrolledAmount = e.CurrentValue;
  276. int scrolledAmount = CalculateContentPosition (sliderScrolledAmount);
  277. RaiseSliderPositionChangeEvents (calculatedSliderPos, _slider.Position);
  278. ContentPosition = _contentPosition + scrolledAmount;
  279. }
  280. /// <summary>
  281. /// Gets or sets the position of the start of the Scroll slider, within the Viewport.
  282. /// </summary>
  283. public int GetSliderPosition () => CalculateSliderPosition (_contentPosition);
  284. private void RaiseSliderPositionChangeEvents (int calculatedSliderPosition, int newSliderPosition)
  285. {
  286. if (calculatedSliderPosition == newSliderPosition)
  287. {
  288. return;
  289. }
  290. // This sets the slider position and clamps the value
  291. _slider.Position = newSliderPosition;
  292. OnSliderPositionChanged (newSliderPosition);
  293. SliderPositionChanged?.Invoke (this, new (in newSliderPosition));
  294. }
  295. /// <summary>Called when the slider position has changed.</summary>
  296. protected virtual void OnSliderPositionChanged (int position) { }
  297. /// <summary>Raised when the slider position has changed.</summary>
  298. public event EventHandler<EventArgs<int>>? SliderPositionChanged;
  299. private int CalculateSliderPosition (int contentPosition, NavigationDirection direction = NavigationDirection.Forward)
  300. {
  301. if (Size - ViewportDimension == 0)
  302. {
  303. return 0;
  304. }
  305. int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
  306. double newSliderPosition = (double)contentPosition / (Size - ViewportDimension) * (scrollBarSize - _slider.Size - _slider.ShrinkBy);
  307. return direction == NavigationDirection.Forward ? (int)Math.Floor (newSliderPosition) : (int)Math.Ceiling (newSliderPosition);
  308. }
  309. private int CalculateContentPosition (int sliderPosition)
  310. {
  311. int scrollBarSize = Orientation == Orientation.Vertical ? Viewport.Height : Viewport.Width;
  312. return (int)Math.Round ((double)(sliderPosition) / (scrollBarSize - _slider.Size - _slider.ShrinkBy) * (Size - ViewportDimension));
  313. }
  314. #endregion SliderPosition
  315. #region ContentPosition
  316. private int _contentPosition;
  317. /// <summary>
  318. /// Gets or sets the position of the slider relative to <see cref="Size"/>.
  319. /// </summary>
  320. /// <remarks>
  321. /// <para>
  322. /// The content position is clamped to 0 and <see cref="Size"/> minus <see cref="ViewportDimension"/>.
  323. /// </para>
  324. /// <para>
  325. /// Setting will result in the <see cref="ContentPositionChanging"/> and <see cref="ContentPositionChanged"/>
  326. /// events being raised.
  327. /// </para>
  328. /// </remarks>
  329. public int ContentPosition
  330. {
  331. get => _contentPosition;
  332. set
  333. {
  334. if (value == _contentPosition)
  335. {
  336. return;
  337. }
  338. // Clamp the value between 0 and Size - ViewportDimension
  339. int newContentPosition = (int)Math.Clamp (value, 0, Math.Max (0, Size - ViewportDimension));
  340. NavigationDirection direction = newContentPosition >= _contentPosition ? NavigationDirection.Forward : NavigationDirection.Backward;
  341. if (OnContentPositionChanging (_contentPosition, newContentPosition))
  342. {
  343. return;
  344. }
  345. CancelEventArgs<int> args = new (ref _contentPosition, ref newContentPosition);
  346. ContentPositionChanging?.Invoke (this, args);
  347. if (args.Cancel)
  348. {
  349. return;
  350. }
  351. int distance = newContentPosition - _contentPosition;
  352. _contentPosition = newContentPosition;
  353. OnContentPositionChanged (_contentPosition);
  354. ContentPositionChanged?.Invoke (this, new (in _contentPosition));
  355. OnScrolled (distance);
  356. Scrolled?.Invoke (this, new (in distance));
  357. int currentSliderPosition = _slider.Position;
  358. int calculatedSliderPosition = CalculateSliderPosition (_contentPosition, direction);
  359. _slider.MoveToPosition (calculatedSliderPosition);
  360. RaiseSliderPositionChangeEvents (currentSliderPosition, _slider.Position);
  361. }
  362. }
  363. /// <summary>
  364. /// Called when <see cref="ContentPosition"/> is changing. Return true to cancel the change.
  365. /// </summary>
  366. protected virtual bool OnContentPositionChanging (int currentPos, int newPos) { return false; }
  367. /// <summary>
  368. /// Raised when the <see cref="ContentPosition"/> is changing. Set <see cref="CancelEventArgs.Cancel"/> to
  369. /// <see langword="true"/> to prevent the position from being changed.
  370. /// </summary>
  371. public event EventHandler<CancelEventArgs<int>>? ContentPositionChanging;
  372. /// <summary>Called when <see cref="ContentPosition"/> has changed.</summary>
  373. protected virtual void OnContentPositionChanged (int position) { }
  374. /// <summary>Raised when the <see cref="ContentPosition"/> has changed.</summary>
  375. public event EventHandler<EventArgs<int>>? ContentPositionChanged;
  376. /// <summary>Called when <see cref="ContentPosition"/> has changed. Indicates how much to scroll.</summary>
  377. protected virtual void OnScrolled (int distance) { }
  378. /// <summary>Raised when the <see cref="ContentPosition"/> has changed. Indicates how much to scroll.</summary>
  379. public event EventHandler<EventArgs<int>>? Scrolled;
  380. #endregion ContentPosition
  381. /// <inheritdoc/>
  382. protected override bool OnClearingViewport ()
  383. {
  384. if (Orientation == Orientation.Vertical)
  385. {
  386. FillRect (Viewport with { Y = Viewport.Y + 1, Height = Viewport.Height - 2 }, Glyphs.Stipple);
  387. }
  388. else
  389. {
  390. FillRect (Viewport with { X = Viewport.X + 1, Width = Viewport.Width - 2 }, Glyphs.Stipple);
  391. }
  392. return true;
  393. }
  394. // TODO: Change this to work OnMouseEvent with continuouse press and grab so it's continous.
  395. /// <inheritdoc/>
  396. protected override bool OnMouseClick (MouseEventArgs args)
  397. {
  398. // Check if the mouse click is a single click
  399. if (!args.IsSingleClicked)
  400. {
  401. return false;
  402. }
  403. int sliderCenter;
  404. int distanceFromCenter;
  405. if (Orientation == Orientation.Vertical)
  406. {
  407. sliderCenter = 1 + _slider.Frame.Y + _slider.Frame.Height / 2;
  408. distanceFromCenter = args.Position.Y - sliderCenter;
  409. }
  410. else
  411. {
  412. sliderCenter = 1 + _slider.Frame.X + _slider.Frame.Width / 2;
  413. distanceFromCenter = args.Position.X - sliderCenter;
  414. }
  415. #if PROPORTIONAL_SCROLL_JUMP
  416. // BUGBUG: This logic mostly works to provide a proportional jump. However, the math
  417. // BUGBUG: falls apart in edge cases. Most other scroll bars (e.g. Windows) do not do prooportional
  418. // BUGBUG: Thus, this is disabled and we just jump a page each click.
  419. // Ratio of the distance to the viewport dimension
  420. double ratio = (double)Math.Abs (distanceFromCenter) / (ViewportDimension);
  421. // Jump size based on the ratio and the total content size
  422. int jump = (int)(ratio * (Size - ViewportDimension));
  423. #else
  424. int jump = (ViewportDimension);
  425. #endif
  426. // Adjust the content position based on the distance
  427. if (distanceFromCenter < 0)
  428. {
  429. ContentPosition = Math.Max (0, ContentPosition - jump);
  430. }
  431. else
  432. {
  433. ContentPosition = Math.Min (Size - _slider.ViewportDimension, ContentPosition + jump);
  434. }
  435. return true;
  436. }
  437. /// <summary>
  438. /// Gets or sets the amount each mouse hweel event will incremenet/decrement the <see cref="ContentPosition"/>.
  439. /// </summary>
  440. /// <remarks>
  441. /// The default is 1.
  442. /// </remarks>
  443. public int Increment { get; set; } = 1;
  444. /// <inheritdoc/>
  445. protected override bool OnMouseEvent (MouseEventArgs mouseEvent)
  446. {
  447. if (SuperView is null)
  448. {
  449. return false;
  450. }
  451. if (!mouseEvent.IsWheel)
  452. {
  453. return false;
  454. }
  455. if (Orientation == Orientation.Vertical)
  456. {
  457. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledDown))
  458. {
  459. ContentPosition += Increment;
  460. }
  461. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledUp))
  462. {
  463. ContentPosition -= Increment;
  464. }
  465. }
  466. else
  467. {
  468. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledRight))
  469. {
  470. ContentPosition += Increment;
  471. }
  472. if (mouseEvent.Flags.HasFlag (MouseFlags.WheeledLeft))
  473. {
  474. ContentPosition -= Increment;
  475. }
  476. }
  477. return true;
  478. }
  479. /// <inheritdoc/>
  480. public bool EnableForDesign ()
  481. {
  482. OrientationChanged += (sender, args) =>
  483. {
  484. if (args.CurrentValue == Orientation.Vertical)
  485. {
  486. Width = 1;
  487. Height = Dim.Fill ();
  488. }
  489. else
  490. {
  491. Width = Dim.Fill ();
  492. Height = 1;
  493. }
  494. };
  495. Width = 1;
  496. Height = Dim.Fill ();
  497. Size = 250;
  498. return true;
  499. }
  500. }