ScrollSlider.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. internal class ScrollSlider : View
  4. {
  5. public ScrollSlider (Scroll host)
  6. {
  7. _host = host;
  8. Id = "scrollSlider";
  9. Width = Dim.Auto (DimAutoStyle.Content);
  10. Height = Dim.Auto (DimAutoStyle.Content);
  11. WantMousePositionReports = true;
  12. }
  13. internal bool _wasSliderMouse;
  14. private readonly Scroll _host;
  15. private int _lastLocation = -1;
  16. private ColorScheme? _savedColorScheme;
  17. public void AdjustSlider ()
  18. {
  19. if (!IsInitialized)
  20. {
  21. return;
  22. }
  23. (int Location, int Dimension) sliderLocationAndDimension = GetSliderLocationDimensionFromPosition ();
  24. X = _host.Orientation == Orientation.Vertical ? 0 : sliderLocationAndDimension.Location;
  25. Y = _host.Orientation == Orientation.Vertical ? sliderLocationAndDimension.Location : 0;
  26. SetContentSize (
  27. new (
  28. _host.Orientation == Orientation.Vertical ? _host.GetContentSize ().Width : sliderLocationAndDimension.Dimension,
  29. _host.Orientation == Orientation.Vertical ? sliderLocationAndDimension.Dimension : _host.GetContentSize ().Height
  30. ));
  31. SetSliderText ();
  32. }
  33. /// <inheritdoc/>
  34. public override Attribute GetNormalColor ()
  35. {
  36. if (_savedColorScheme is null)
  37. {
  38. ColorScheme = new () { Normal = new (_host.ColorScheme.HotNormal.Foreground, _host.ColorScheme.HotNormal.Foreground) };
  39. }
  40. else
  41. {
  42. ColorScheme = new () { Normal = new (_host.ColorScheme.Normal.Foreground, _host.ColorScheme.Normal.Foreground) };
  43. }
  44. return base.GetNormalColor ();
  45. }
  46. /// <inheritdoc/>
  47. protected internal override bool? OnMouseEnter (MouseEvent mouseEvent)
  48. {
  49. _savedColorScheme ??= _host.ColorScheme;
  50. ColorScheme = new ()
  51. {
  52. Normal = new (_savedColorScheme.HotNormal.Foreground, _savedColorScheme.HotNormal.Foreground),
  53. Focus = new (_savedColorScheme.Focus.Foreground, _savedColorScheme.Focus.Foreground),
  54. HotNormal = new (_savedColorScheme.Normal.Foreground, _savedColorScheme.Normal.Foreground),
  55. HotFocus = new (_savedColorScheme.HotFocus.Foreground, _savedColorScheme.HotFocus.Foreground),
  56. Disabled = new (_savedColorScheme.Disabled.Foreground, _savedColorScheme.Disabled.Foreground)
  57. };
  58. return base.OnMouseEnter (mouseEvent);
  59. }
  60. /// <inheritdoc/>
  61. protected internal override bool OnMouseEvent (MouseEvent mouseEvent)
  62. {
  63. if (!_host._wasSliderLayoutComplete)
  64. {
  65. // Ensure not blocking scroll mouse event
  66. _host._wasSliderLayoutComplete = true;
  67. }
  68. int location = _host.Orientation == Orientation.Vertical ? mouseEvent.Position.Y : mouseEvent.Position.X;
  69. int offset = _lastLocation > -1 ? location - _lastLocation : 0;
  70. int barSize = _host.Orientation == Orientation.Vertical ? _host.GetContentSize ().Height : _host.GetContentSize ().Width;
  71. if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) && _lastLocation == -1)
  72. {
  73. if (Application.MouseGrabView != this)
  74. {
  75. Application.GrabMouse (this);
  76. _lastLocation = location;
  77. }
  78. }
  79. else if (mouseEvent.Flags == (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))
  80. {
  81. _wasSliderMouse = true;
  82. if (_host.Orientation == Orientation.Vertical)
  83. {
  84. Y = Frame.Y + offset < 0 ? 0 :
  85. Frame.Y + offset + Frame.Height > barSize ? Math.Max (barSize - Frame.Height, 0) : Frame.Y + offset;
  86. _host.Position = GetPositionFromSliderLocation (Frame.Y);
  87. }
  88. else
  89. {
  90. X = Frame.X + offset < 0 ? 0 :
  91. Frame.X + offset + Frame.Width > barSize ? Math.Max (barSize - Frame.Width, 0) : Frame.X + offset;
  92. _host.Position = GetPositionFromSliderLocation (Frame.X);
  93. }
  94. }
  95. else if (mouseEvent.Flags == MouseFlags.Button1Released)
  96. {
  97. _lastLocation = -1;
  98. if (Application.MouseGrabView == this)
  99. {
  100. Application.UngrabMouse ();
  101. }
  102. }
  103. else if ((mouseEvent.Flags == MouseFlags.WheeledDown && _host.Orientation == Orientation.Vertical)
  104. || (mouseEvent.Flags == MouseFlags.WheeledRight && _host.Orientation == Orientation.Horizontal))
  105. {
  106. _host.Position = Math.Min (_host.Position + 1, _host.Size - barSize);
  107. }
  108. else if ((mouseEvent.Flags == MouseFlags.WheeledUp && _host.Orientation == Orientation.Vertical)
  109. || (mouseEvent.Flags == MouseFlags.WheeledLeft && _host.Orientation == Orientation.Horizontal))
  110. {
  111. _host.Position = Math.Max (_host.Position - 1, 0);
  112. }
  113. else if (mouseEvent.Flags != MouseFlags.ReportMousePosition)
  114. {
  115. return base.OnMouseEvent (mouseEvent);
  116. }
  117. return true;
  118. }
  119. /// <inheritdoc/>
  120. protected internal override bool OnMouseLeave (MouseEvent mouseEvent)
  121. {
  122. if (_savedColorScheme is { } && !mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed))
  123. {
  124. ColorScheme = _savedColorScheme;
  125. _savedColorScheme = null;
  126. }
  127. return base.OnMouseLeave (mouseEvent);
  128. }
  129. /// <inheritdoc />
  130. internal override void OnLayoutComplete (LayoutEventArgs args)
  131. {
  132. base.OnLayoutComplete (args);
  133. _host._wasSliderLayoutComplete = true;
  134. }
  135. private int GetPositionFromSliderLocation (int location)
  136. {
  137. if (_host.GetContentSize ().Height == 0 || _host.GetContentSize ().Width == 0)
  138. {
  139. return 0;
  140. }
  141. int scrollSize = _host.Orientation == Orientation.Vertical ? _host.GetContentSize ().Height : _host.GetContentSize ().Width;
  142. // Ensure the Position is valid if the slider is at end
  143. // We use Frame here instead of ContentSize because even if the slider has a margin or border, Frame indicates the actual size
  144. if ((_host.Orientation == Orientation.Vertical && location + Frame.Height == scrollSize)
  145. || (_host.Orientation == Orientation.Horizontal && location + Frame.Width == scrollSize))
  146. {
  147. return _host.Size - scrollSize;
  148. }
  149. return Math.Min ((location * _host.Size + location) / scrollSize, _host.Size - scrollSize);
  150. }
  151. // QUESTION: This method is only called from one place. Should it be inlined? Or, should it be made internal and unit tests be provided?
  152. private (int Location, int Dimension) GetSliderLocationDimensionFromPosition ()
  153. {
  154. if (_host.GetContentSize ().Height == 0 || _host.GetContentSize ().Width == 0)
  155. {
  156. return new (0, 0);
  157. }
  158. int scrollSize = _host.Orientation == Orientation.Vertical ? _host.GetContentSize ().Height : _host.GetContentSize ().Width;
  159. int location;
  160. int dimension;
  161. if (_host.Size > 0)
  162. {
  163. dimension = (int)Math.Min (Math.Max (Math.Ceiling ((double)scrollSize * scrollSize / _host.Size), 1), scrollSize);
  164. // Ensure the Position is valid
  165. if (_host.Position > 0 && _host.Position + scrollSize > _host.Size)
  166. {
  167. _host.Position = _host.Size - scrollSize;
  168. }
  169. location = Math.Min ((_host.Position * scrollSize + _host.Position) / _host.Size, scrollSize - dimension);
  170. if (_host.Position == _host.Size - scrollSize && location + dimension < scrollSize)
  171. {
  172. location = scrollSize - dimension;
  173. }
  174. }
  175. else
  176. {
  177. location = 0;
  178. dimension = scrollSize;
  179. }
  180. return new (location, dimension);
  181. }
  182. // TODO: I think you should create a new `internal` view named "ScrollSlider" with an `Orientation` property. It should inherit from View and override GetNormalColor and the mouse events
  183. // that can be moved within it's Superview, constrained to move only horizontally or vertically depending on Orientation.
  184. // This will really simplify a lot of this.
  185. private void SetSliderText ()
  186. {
  187. TextDirection = _host.Orientation == Orientation.Vertical ? TextDirection.TopBottom_LeftRight : TextDirection.LeftRight_TopBottom;
  188. // QUESTION: Should these Glyphs be configurable via CM?
  189. Text = string.Concat (
  190. Enumerable.Repeat (
  191. Glyphs.ContinuousMeterSegment.ToString (),
  192. _host.GetContentSize ().Width * _host.GetContentSize ().Height));
  193. }
  194. }