Slider.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. using System;
  2. using Avalonia.Collections;
  3. using Avalonia.Controls.Mixins;
  4. using Avalonia.Controls.Primitives;
  5. using Avalonia.Input;
  6. using Avalonia.Interactivity;
  7. using Avalonia.Layout;
  8. using Avalonia.Utilities;
  9. namespace Avalonia.Controls
  10. {
  11. /// <summary>
  12. /// Enum which describes how to position the ticks in a <see cref="Slider"/>.
  13. /// </summary>
  14. public enum TickPlacement
  15. {
  16. /// <summary>
  17. /// No tick marks will appear.
  18. /// </summary>
  19. None,
  20. /// <summary>
  21. /// Tick marks will appear above the track for a horizontal <see cref="Slider"/>, or to the left of the track for a vertical <see cref="Slider"/>.
  22. /// </summary>
  23. TopLeft,
  24. /// <summary>
  25. /// Tick marks will appear below the track for a horizontal <see cref="Slider"/>, or to the right of the track for a vertical <see cref="Slider"/>.
  26. /// </summary>
  27. BottomRight,
  28. /// <summary>
  29. /// Tick marks appear on both sides of either a horizontal or vertical <see cref="Slider"/>.
  30. /// </summary>
  31. Outside
  32. }
  33. /// <summary>
  34. /// A control that lets the user select from a range of values by moving a Thumb control along a Track.
  35. /// </summary>
  36. public class Slider : RangeBase
  37. {
  38. /// <summary>
  39. /// Defines the <see cref="Orientation"/> property.
  40. /// </summary>
  41. public static readonly StyledProperty<Orientation> OrientationProperty =
  42. ScrollBar.OrientationProperty.AddOwner<Slider>();
  43. /// <summary>
  44. /// Defines the <see cref="IsSnapToTickEnabled"/> property.
  45. /// </summary>
  46. public static readonly StyledProperty<bool> IsSnapToTickEnabledProperty =
  47. AvaloniaProperty.Register<Slider, bool>(nameof(IsSnapToTickEnabled), false);
  48. /// <summary>
  49. /// Defines the <see cref="TickFrequency"/> property.
  50. /// </summary>
  51. public static readonly StyledProperty<double> TickFrequencyProperty =
  52. AvaloniaProperty.Register<Slider, double>(nameof(TickFrequency), 0.0);
  53. /// <summary>
  54. /// Defines the <see cref="TickPlacement"/> property.
  55. /// </summary>
  56. public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
  57. AvaloniaProperty.Register<TickBar, TickPlacement>(nameof(TickPlacement), 0d);
  58. /// <summary>
  59. /// Defines the <see cref="TicksProperty"/> property.
  60. /// </summary>
  61. public static readonly StyledProperty<AvaloniaList<double>> TicksProperty =
  62. TickBar.TicksProperty.AddOwner<Slider>();
  63. // Slider required parts
  64. private bool _isDragging = false;
  65. private Track _track;
  66. private Button _decreaseButton;
  67. private Button _increaseButton;
  68. private IDisposable _decreaseButtonPressDispose;
  69. private IDisposable _decreaseButtonReleaseDispose;
  70. private IDisposable _increaseButtonSubscription;
  71. private IDisposable _increaseButtonReleaseDispose;
  72. private IDisposable _pointerMovedDispose;
  73. /// <summary>
  74. /// Initializes static members of the <see cref="Slider"/> class.
  75. /// </summary>
  76. static Slider()
  77. {
  78. PressedMixin.Attach<Slider>();
  79. OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
  80. Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
  81. Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e),
  82. RoutingStrategies.Bubble);
  83. }
  84. /// <summary>
  85. /// Instantiates a new instance of the <see cref="Slider"/> class.
  86. /// </summary>
  87. public Slider()
  88. {
  89. UpdatePseudoClasses(Orientation);
  90. }
  91. /// <summary>
  92. /// Defines the ticks to be drawn on the tick bar.
  93. /// </summary>
  94. public AvaloniaList<double> Ticks
  95. {
  96. get => GetValue(TicksProperty);
  97. set => SetValue(TicksProperty, value);
  98. }
  99. /// <summary>
  100. /// Gets or sets the orientation of a <see cref="Slider"/>.
  101. /// </summary>
  102. public Orientation Orientation
  103. {
  104. get { return GetValue(OrientationProperty); }
  105. set { SetValue(OrientationProperty, value); }
  106. }
  107. /// <summary>
  108. /// Gets or sets a value that indicates whether the <see cref="Slider"/> automatically moves the <see cref="Thumb"/> to the closest tick mark.
  109. /// </summary>
  110. public bool IsSnapToTickEnabled
  111. {
  112. get { return GetValue(IsSnapToTickEnabledProperty); }
  113. set { SetValue(IsSnapToTickEnabledProperty, value); }
  114. }
  115. /// <summary>
  116. /// Gets or sets the interval between tick marks.
  117. /// </summary>
  118. public double TickFrequency
  119. {
  120. get { return GetValue(TickFrequencyProperty); }
  121. set { SetValue(TickFrequencyProperty, value); }
  122. }
  123. /// <summary>
  124. /// Gets or sets a value that indicates where to draw
  125. /// tick marks in relation to the track.
  126. /// </summary>
  127. public TickPlacement TickPlacement
  128. {
  129. get { return GetValue(TickPlacementProperty); }
  130. set { SetValue(TickPlacementProperty, value); }
  131. }
  132. /// <inheritdoc/>
  133. protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
  134. {
  135. base.OnApplyTemplate(e);
  136. _decreaseButtonPressDispose?.Dispose();
  137. _decreaseButtonReleaseDispose?.Dispose();
  138. _increaseButtonSubscription?.Dispose();
  139. _increaseButtonReleaseDispose?.Dispose();
  140. _pointerMovedDispose?.Dispose();
  141. _decreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");
  142. _track = e.NameScope.Find<Track>("PART_Track");
  143. _increaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
  144. if (_track != null)
  145. {
  146. _track.IsThumbDragHandled = true;
  147. }
  148. if (_decreaseButton != null)
  149. {
  150. _decreaseButtonPressDispose = _decreaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
  151. _decreaseButtonReleaseDispose = _decreaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
  152. }
  153. if (_increaseButton != null)
  154. {
  155. _increaseButtonSubscription = _increaseButton.AddDisposableHandler(PointerPressedEvent, TrackPressed, RoutingStrategies.Tunnel);
  156. _increaseButtonReleaseDispose = _increaseButton.AddDisposableHandler(PointerReleasedEvent, TrackReleased, RoutingStrategies.Tunnel);
  157. }
  158. _pointerMovedDispose = this.AddDisposableHandler(PointerMovedEvent, TrackMoved, RoutingStrategies.Tunnel);
  159. }
  160. private void TrackMoved(object sender, PointerEventArgs e)
  161. {
  162. if (_isDragging)
  163. {
  164. MoveToPoint(e.GetCurrentPoint(_track));
  165. }
  166. }
  167. private void TrackReleased(object sender, PointerReleasedEventArgs e)
  168. {
  169. _isDragging = false;
  170. }
  171. private void TrackPressed(object sender, PointerPressedEventArgs e)
  172. {
  173. if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
  174. {
  175. MoveToPoint(e.GetCurrentPoint(_track));
  176. _isDragging = true;
  177. }
  178. }
  179. private void MoveToPoint(PointerPoint x)
  180. {
  181. var orient = Orientation == Orientation.Horizontal;
  182. var pointDen = orient ? _track.Bounds.Width : _track.Bounds.Height;
  183. // Just add epsilon to avoid NaN in case 0/0
  184. pointDen += double.Epsilon;
  185. var pointNum = orient ? x.Position.X : x.Position.Y;
  186. var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d);
  187. var invert = orient ? 0 : 1;
  188. var calcVal = Math.Abs(invert - logicalPos);
  189. var range = Maximum - Minimum;
  190. var finalValue = calcVal * range + Minimum;
  191. Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue;
  192. }
  193. protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
  194. {
  195. base.OnPropertyChanged(change);
  196. if (change.Property == OrientationProperty)
  197. {
  198. UpdatePseudoClasses(change.NewValue.GetValueOrDefault<Orientation>());
  199. }
  200. }
  201. /// <summary>
  202. /// Called when user start dragging the <see cref="Thumb"/>.
  203. /// </summary>
  204. /// <param name="e"></param>
  205. protected virtual void OnThumbDragStarted(VectorEventArgs e)
  206. {
  207. _isDragging = true;
  208. }
  209. /// <summary>
  210. /// Called when user stop dragging the <see cref="Thumb"/>.
  211. /// </summary>
  212. /// <param name="e"></param>
  213. protected virtual void OnThumbDragCompleted(VectorEventArgs e)
  214. {
  215. _isDragging = false;
  216. }
  217. /// <summary>
  218. /// Snap the input 'value' to the closest tick.
  219. /// </summary>
  220. /// <param name="value">Value that want to snap to closest Tick.</param>
  221. private double SnapToTick(double value)
  222. {
  223. if (IsSnapToTickEnabled)
  224. {
  225. double previous = Minimum;
  226. double next = Maximum;
  227. // This property is rarely set so let's try to avoid the GetValue
  228. var ticks = Ticks;
  229. // If ticks collection is available, use it.
  230. // Note that ticks may be unsorted.
  231. if ((ticks != null) && (ticks.Count > 0))
  232. {
  233. for (int i = 0; i < ticks.Count; i++)
  234. {
  235. double tick = ticks[i];
  236. if (MathUtilities.AreClose(tick, value))
  237. {
  238. return value;
  239. }
  240. if (MathUtilities.LessThan(tick, value) && MathUtilities.GreaterThan(tick, previous))
  241. {
  242. previous = tick;
  243. }
  244. else if (MathUtilities.GreaterThan(tick, value) && MathUtilities.LessThan(tick, next))
  245. {
  246. next = tick;
  247. }
  248. }
  249. }
  250. else if (MathUtilities.GreaterThan(TickFrequency, 0.0))
  251. {
  252. previous = Minimum + (Math.Round(((value - Minimum) / TickFrequency)) * TickFrequency);
  253. next = Math.Min(Maximum, previous + TickFrequency);
  254. }
  255. // Choose the closest value between previous and next. If tie, snap to 'next'.
  256. value = MathUtilities.GreaterThanOrClose(value, (previous + next) * 0.5) ? next : previous;
  257. }
  258. return value;
  259. }
  260. private void UpdatePseudoClasses(Orientation o)
  261. {
  262. PseudoClasses.Set(":vertical", o == Orientation.Vertical);
  263. PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
  264. }
  265. }
  266. }