ScrollContentPresenter.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. // Copyright (c) The Avalonia Project. All rights reserved.
  2. // Licensed under the MIT license. See licence.md file in the project root for full license information.
  3. using System;
  4. using System.Linq;
  5. using System.Reactive.Disposables;
  6. using System.Reactive.Linq;
  7. using Avalonia.Controls.Primitives;
  8. using Avalonia.Input;
  9. using Avalonia.Layout;
  10. using Avalonia.VisualTree;
  11. namespace Avalonia.Controls.Presenters
  12. {
  13. /// <summary>
  14. /// Presents a scrolling view of content inside a <see cref="ScrollViewer"/>.
  15. /// </summary>
  16. public class ScrollContentPresenter : ContentPresenter, IPresenter, IScrollable
  17. {
  18. /// <summary>
  19. /// Defines the <see cref="CanHorizontallyScroll"/> property.
  20. /// </summary>
  21. public static readonly DirectProperty<ScrollContentPresenter, bool> CanHorizontallyScrollProperty =
  22. AvaloniaProperty.RegisterDirect<ScrollContentPresenter, bool>(
  23. nameof(CanHorizontallyScroll),
  24. o => o.CanHorizontallyScroll,
  25. (o, v) => o.CanHorizontallyScroll = v);
  26. /// <summary>
  27. /// Defines the <see cref="CanVerticallyScroll"/> property.
  28. /// </summary>
  29. public static readonly DirectProperty<ScrollContentPresenter, bool> CanVerticallyScrollProperty =
  30. AvaloniaProperty.RegisterDirect<ScrollContentPresenter, bool>(
  31. nameof(CanVerticallyScroll),
  32. o => o.CanVerticallyScroll,
  33. (o, v) => o.CanVerticallyScroll = v);
  34. /// <summary>
  35. /// Defines the <see cref="Extent"/> property.
  36. /// </summary>
  37. public static readonly DirectProperty<ScrollContentPresenter, Size> ExtentProperty =
  38. ScrollViewer.ExtentProperty.AddOwner<ScrollContentPresenter>(
  39. o => o.Extent,
  40. (o, v) => o.Extent = v);
  41. /// <summary>
  42. /// Defines the <see cref="Offset"/> property.
  43. /// </summary>
  44. public static readonly DirectProperty<ScrollContentPresenter, Vector> OffsetProperty =
  45. ScrollViewer.OffsetProperty.AddOwner<ScrollContentPresenter>(
  46. o => o.Offset,
  47. (o, v) => o.Offset = v);
  48. /// <summary>
  49. /// Defines the <see cref="Viewport"/> property.
  50. /// </summary>
  51. public static readonly DirectProperty<ScrollContentPresenter, Size> ViewportProperty =
  52. ScrollViewer.ViewportProperty.AddOwner<ScrollContentPresenter>(
  53. o => o.Viewport,
  54. (o, v) => o.Viewport = v);
  55. private bool _canHorizontallyScroll;
  56. private bool _canVerticallyScroll;
  57. private Size _extent;
  58. private Vector _offset;
  59. private IDisposable _logicalScrollSubscription;
  60. private Size _viewport;
  61. /// <summary>
  62. /// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
  63. /// </summary>
  64. static ScrollContentPresenter()
  65. {
  66. ClipToBoundsProperty.OverrideDefaultValue(typeof(ScrollContentPresenter), true);
  67. ChildProperty.Changed.AddClassHandler<ScrollContentPresenter>(x => x.ChildChanged);
  68. AffectsArrange(OffsetProperty);
  69. }
  70. /// <summary>
  71. /// Initializes a new instance of the <see cref="ScrollContentPresenter"/> class.
  72. /// </summary>
  73. public ScrollContentPresenter()
  74. {
  75. AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
  76. this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
  77. }
  78. /// <summary>
  79. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
  80. /// </summary>
  81. public bool CanHorizontallyScroll
  82. {
  83. get { return _canHorizontallyScroll; }
  84. set { SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value); }
  85. }
  86. /// <summary>
  87. /// Gets or sets a value indicating whether the content can be scrolled horizontally.
  88. /// </summary>
  89. public bool CanVerticallyScroll
  90. {
  91. get { return _canVerticallyScroll; }
  92. set { SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value); }
  93. }
  94. /// <summary>
  95. /// Gets the extent of the scrollable content.
  96. /// </summary>
  97. public Size Extent
  98. {
  99. get { return _extent; }
  100. private set { SetAndRaise(ExtentProperty, ref _extent, value); }
  101. }
  102. /// <summary>
  103. /// Gets or sets the current scroll offset.
  104. /// </summary>
  105. public Vector Offset
  106. {
  107. get { return _offset; }
  108. set { SetAndRaise(OffsetProperty, ref _offset, value); }
  109. }
  110. /// <summary>
  111. /// Gets the size of the viewport on the scrollable content.
  112. /// </summary>
  113. public Size Viewport
  114. {
  115. get { return _viewport; }
  116. private set { SetAndRaise(ViewportProperty, ref _viewport, value); }
  117. }
  118. /// <summary>
  119. /// Attempts to bring a portion of the target visual into view by scrolling the content.
  120. /// </summary>
  121. /// <param name="target">The target visual.</param>
  122. /// <param name="targetRect">The portion of the target visual to bring into view.</param>
  123. /// <returns>True if the scroll offset was changed; otherwise false.</returns>
  124. public bool BringDescendantIntoView(IVisual target, Rect targetRect)
  125. {
  126. if (Child == null)
  127. {
  128. return false;
  129. }
  130. var scrollable = Child as ILogicalScrollable;
  131. var control = target as IControl;
  132. if (scrollable?.IsLogicalScrollEnabled == true && control != null)
  133. {
  134. return scrollable.BringIntoView(control, targetRect);
  135. }
  136. var transform = target.TransformToVisual(Child);
  137. if (transform == null)
  138. {
  139. return false;
  140. }
  141. var rect = targetRect.TransformToAABB(transform.Value);
  142. var offset = Offset;
  143. var result = false;
  144. if (rect.Bottom > offset.Y + Viewport.Height)
  145. {
  146. offset = offset.WithY((rect.Bottom - Viewport.Height) + Child.Margin.Top);
  147. result = true;
  148. }
  149. if (rect.Y < offset.Y)
  150. {
  151. offset = offset.WithY(rect.Y);
  152. result = true;
  153. }
  154. if (rect.Right > offset.X + Viewport.Width)
  155. {
  156. offset = offset.WithX((rect.Right - Viewport.Width) + Child.Margin.Left);
  157. result = true;
  158. }
  159. if (rect.X < offset.X)
  160. {
  161. offset = offset.WithX(rect.X);
  162. result = true;
  163. }
  164. if (result)
  165. {
  166. Offset = offset;
  167. }
  168. return result;
  169. }
  170. /// <inheritdoc/>
  171. protected override Size MeasureOverride(Size availableSize)
  172. {
  173. if (_logicalScrollSubscription != null || Child == null)
  174. {
  175. return base.MeasureOverride(availableSize);
  176. }
  177. var constraint = new Size(
  178. CanHorizontallyScroll ? double.PositiveInfinity : availableSize.Width,
  179. CanVerticallyScroll ? double.PositiveInfinity : availableSize.Height);
  180. Child.Measure(constraint);
  181. return Child.DesiredSize.Constrain(availableSize);
  182. }
  183. /// <inheritdoc/>
  184. protected override Size ArrangeOverride(Size finalSize)
  185. {
  186. if (_logicalScrollSubscription != null || Child == null)
  187. {
  188. return base.ArrangeOverride(finalSize);
  189. }
  190. var size = new Size(
  191. CanHorizontallyScroll ? Math.Max(Child.DesiredSize.Width, finalSize.Width) : finalSize.Width,
  192. CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height);
  193. ArrangeOverrideImpl(size, -Offset);
  194. Viewport = finalSize;
  195. Extent = Child.Bounds.Size.Inflate(Child.Margin);
  196. return finalSize;
  197. }
  198. /// <inheritdoc/>
  199. protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
  200. {
  201. if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
  202. {
  203. var scrollable = Child as ILogicalScrollable;
  204. bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
  205. double x = Offset.X;
  206. double y = Offset.Y;
  207. if (Extent.Height > Viewport.Height)
  208. {
  209. double height = isLogical ? scrollable.ScrollSize.Height : 50;
  210. y += -e.Delta.Y * height;
  211. y = Math.Max(y, 0);
  212. y = Math.Min(y, Extent.Height - Viewport.Height);
  213. }
  214. if (Extent.Width > Viewport.Width)
  215. {
  216. double width = isLogical ? scrollable.ScrollSize.Width : 50;
  217. x += -e.Delta.X * width;
  218. x = Math.Max(x, 0);
  219. x = Math.Min(x, Extent.Width - Viewport.Width);
  220. }
  221. Offset = new Vector(x, y);
  222. e.Handled = true;
  223. }
  224. }
  225. private void BringIntoViewRequested(object sender, RequestBringIntoViewEventArgs e)
  226. {
  227. e.Handled = BringDescendantIntoView(e.TargetObject, e.TargetRect);
  228. }
  229. private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
  230. {
  231. UpdateScrollableSubscription((IControl)e.NewValue);
  232. if (e.OldValue != null)
  233. {
  234. Offset = default(Vector);
  235. }
  236. }
  237. private void UpdateScrollableSubscription(IControl child)
  238. {
  239. var scrollable = child as ILogicalScrollable;
  240. _logicalScrollSubscription?.Dispose();
  241. _logicalScrollSubscription = null;
  242. if (scrollable != null)
  243. {
  244. scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable);
  245. if (scrollable.IsLogicalScrollEnabled == true)
  246. {
  247. _logicalScrollSubscription = new CompositeDisposable(
  248. this.GetObservable(CanHorizontallyScrollProperty)
  249. .Subscribe(x => scrollable.CanHorizontallyScroll = x),
  250. this.GetObservable(CanVerticallyScrollProperty)
  251. .Subscribe(x => scrollable.CanVerticallyScroll = x),
  252. this.GetObservable(OffsetProperty)
  253. .Skip(1).Subscribe(x => scrollable.Offset = x),
  254. Disposable.Create(() => scrollable.InvalidateScroll = null));
  255. UpdateFromScrollable(scrollable);
  256. }
  257. }
  258. }
  259. private void UpdateFromScrollable(ILogicalScrollable scrollable)
  260. {
  261. var logicalScroll = _logicalScrollSubscription != null;
  262. if (logicalScroll != scrollable.IsLogicalScrollEnabled)
  263. {
  264. UpdateScrollableSubscription(Child);
  265. Offset = default(Vector);
  266. InvalidateMeasure();
  267. }
  268. else if (scrollable.IsLogicalScrollEnabled)
  269. {
  270. Viewport = scrollable.Viewport;
  271. Extent = scrollable.Extent;
  272. Offset = scrollable.Offset;
  273. }
  274. }
  275. }
  276. }