Scroll.cs 18 KB

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