LayoutManager.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. using System;
  2. using System.Buffers;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using Avalonia.Logging;
  6. using Avalonia.Threading;
  7. using Avalonia.VisualTree;
  8. #nullable enable
  9. namespace Avalonia.Layout
  10. {
  11. /// <summary>
  12. /// Manages measuring and arranging of controls.
  13. /// </summary>
  14. public class LayoutManager : ILayoutManager, IDisposable
  15. {
  16. private const int MaxPasses = 3;
  17. private readonly ILayoutRoot _owner;
  18. private readonly LayoutQueue<ILayoutable> _toMeasure = new LayoutQueue<ILayoutable>(v => !v.IsMeasureValid);
  19. private readonly LayoutQueue<ILayoutable> _toArrange = new LayoutQueue<ILayoutable>(v => !v.IsArrangeValid);
  20. private readonly Action _executeLayoutPass;
  21. private List<EffectiveViewportChangedListener>? _effectiveViewportChangedListeners;
  22. private bool _disposed;
  23. private bool _queued;
  24. private bool _running;
  25. public LayoutManager(ILayoutRoot owner)
  26. {
  27. _owner = owner ?? throw new ArgumentNullException(nameof(owner));
  28. _executeLayoutPass = ExecuteQueuedLayoutPass;
  29. }
  30. public virtual event EventHandler? LayoutUpdated;
  31. /// <inheritdoc/>
  32. public virtual void InvalidateMeasure(ILayoutable control)
  33. {
  34. control = control ?? throw new ArgumentNullException(nameof(control));
  35. Dispatcher.UIThread.VerifyAccess();
  36. if (_disposed)
  37. {
  38. return;
  39. }
  40. if (!control.IsAttachedToVisualTree)
  41. {
  42. #if DEBUG
  43. throw new AvaloniaInternalException(
  44. "LayoutManager.InvalidateMeasure called on a control that is detached from the visual tree.");
  45. #else
  46. return;
  47. #endif
  48. }
  49. if (control.VisualRoot != _owner)
  50. {
  51. throw new ArgumentException("Attempt to call InvalidateMeasure on wrong LayoutManager.");
  52. }
  53. _toMeasure.Enqueue(control);
  54. _toArrange.Enqueue(control);
  55. QueueLayoutPass();
  56. }
  57. /// <inheritdoc/>
  58. public virtual void InvalidateArrange(ILayoutable control)
  59. {
  60. control = control ?? throw new ArgumentNullException(nameof(control));
  61. Dispatcher.UIThread.VerifyAccess();
  62. if (_disposed)
  63. {
  64. return;
  65. }
  66. if (!control.IsAttachedToVisualTree)
  67. {
  68. #if DEBUG
  69. throw new AvaloniaInternalException(
  70. "LayoutManager.InvalidateArrange called on a control that is detached from the visual tree.");
  71. #else
  72. return;
  73. #endif
  74. }
  75. if (control.VisualRoot != _owner)
  76. {
  77. throw new ArgumentException("Attempt to call InvalidateArrange on wrong LayoutManager.");
  78. }
  79. _toArrange.Enqueue(control);
  80. QueueLayoutPass();
  81. }
  82. private void ExecuteQueuedLayoutPass()
  83. {
  84. if (!_queued)
  85. {
  86. return;
  87. }
  88. ExecuteLayoutPass();
  89. }
  90. /// <inheritdoc/>
  91. public virtual void ExecuteLayoutPass()
  92. {
  93. Dispatcher.UIThread.VerifyAccess();
  94. if (_disposed)
  95. {
  96. return;
  97. }
  98. if (!_running)
  99. {
  100. Stopwatch? stopwatch = null;
  101. const LogEventLevel timingLogLevel = LogEventLevel.Information;
  102. bool captureTiming = Logger.IsEnabled(timingLogLevel, LogArea.Layout);
  103. if (captureTiming)
  104. {
  105. Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(
  106. this,
  107. "Started layout pass. To measure: {Measure} To arrange: {Arrange}",
  108. _toMeasure.Count,
  109. _toArrange.Count);
  110. stopwatch = new Stopwatch();
  111. stopwatch.Start();
  112. }
  113. _toMeasure.BeginLoop(MaxPasses);
  114. _toArrange.BeginLoop(MaxPasses);
  115. try
  116. {
  117. _running = true;
  118. for (var pass = 0; pass < MaxPasses; ++pass)
  119. {
  120. InnerLayoutPass();
  121. if (!RaiseEffectiveViewportChanged())
  122. {
  123. break;
  124. }
  125. }
  126. }
  127. finally
  128. {
  129. _running = false;
  130. }
  131. _toMeasure.EndLoop();
  132. _toArrange.EndLoop();
  133. if (captureTiming)
  134. {
  135. stopwatch!.Stop();
  136. Logger.TryGet(timingLogLevel, LogArea.Layout)?.Log(this, "Layout pass finished in {Time}", stopwatch.Elapsed);
  137. }
  138. }
  139. _queued = false;
  140. LayoutUpdated?.Invoke(this, EventArgs.Empty);
  141. }
  142. /// <inheritdoc/>
  143. public virtual void ExecuteInitialLayoutPass()
  144. {
  145. if (_disposed)
  146. {
  147. return;
  148. }
  149. try
  150. {
  151. _running = true;
  152. Measure(_owner);
  153. Arrange(_owner);
  154. }
  155. finally
  156. {
  157. _running = false;
  158. }
  159. // Running the initial layout pass may have caused some control to be invalidated
  160. // so run a full layout pass now (this usually due to scrollbars; its not known
  161. // whether they will need to be shown until the layout pass has run and if the
  162. // first guess was incorrect the layout will need to be updated).
  163. ExecuteLayoutPass();
  164. }
  165. [Obsolete("Call ExecuteInitialLayoutPass without parameter")]
  166. public void ExecuteInitialLayoutPass(ILayoutRoot root)
  167. {
  168. if (root != _owner)
  169. {
  170. throw new ArgumentException("ExecuteInitialLayoutPass called with incorrect root.");
  171. }
  172. ExecuteInitialLayoutPass();
  173. }
  174. public void Dispose()
  175. {
  176. _disposed = true;
  177. _toMeasure.Dispose();
  178. _toArrange.Dispose();
  179. }
  180. void ILayoutManager.RegisterEffectiveViewportListener(ILayoutable control)
  181. {
  182. _effectiveViewportChangedListeners ??= new List<EffectiveViewportChangedListener>();
  183. _effectiveViewportChangedListeners.Add(new EffectiveViewportChangedListener(
  184. control,
  185. CalculateEffectiveViewport(control)));
  186. }
  187. void ILayoutManager.UnregisterEffectiveViewportListener(ILayoutable control)
  188. {
  189. if (_effectiveViewportChangedListeners is object)
  190. {
  191. for (var i = _effectiveViewportChangedListeners.Count - 1; i >= 0; --i)
  192. {
  193. if (_effectiveViewportChangedListeners[i].Listener == control)
  194. {
  195. _effectiveViewportChangedListeners.RemoveAt(i);
  196. }
  197. }
  198. }
  199. }
  200. private void InnerLayoutPass()
  201. {
  202. for (var pass = 0; pass < MaxPasses; ++pass)
  203. {
  204. ExecuteMeasurePass();
  205. ExecuteArrangePass();
  206. if (_toMeasure.Count == 0)
  207. {
  208. break;
  209. }
  210. }
  211. }
  212. private void ExecuteMeasurePass()
  213. {
  214. while (_toMeasure.Count > 0)
  215. {
  216. var control = _toMeasure.Dequeue();
  217. if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
  218. {
  219. Measure(control);
  220. }
  221. }
  222. }
  223. private void ExecuteArrangePass()
  224. {
  225. while (_toArrange.Count > 0)
  226. {
  227. var control = _toArrange.Dequeue();
  228. if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
  229. {
  230. Arrange(control);
  231. }
  232. }
  233. }
  234. private void Measure(ILayoutable control)
  235. {
  236. // Controls closest to the visual root need to be arranged first. We don't try to store
  237. // ordered invalidation lists, instead we traverse the tree upwards, measuring the
  238. // controls closest to the root first. This has been shown by benchmarks to be the
  239. // fastest and most memory-efficient algorithm.
  240. if (control.VisualParent is ILayoutable parent)
  241. {
  242. Measure(parent);
  243. }
  244. // If the control being measured has IsMeasureValid == true here then its measure was
  245. // handed by an ancestor and can be ignored. The measure may have also caused the
  246. // control to be removed.
  247. if (!control.IsMeasureValid && control.IsAttachedToVisualTree)
  248. {
  249. if (control is ILayoutRoot root)
  250. {
  251. root.Measure(Size.Infinity);
  252. }
  253. else if (control.PreviousMeasure.HasValue)
  254. {
  255. control.Measure(control.PreviousMeasure.Value);
  256. }
  257. }
  258. }
  259. private void Arrange(ILayoutable control)
  260. {
  261. if (control.VisualParent is ILayoutable parent)
  262. {
  263. Arrange(parent);
  264. }
  265. if (!control.IsArrangeValid && control.IsAttachedToVisualTree)
  266. {
  267. if (control is IEmbeddedLayoutRoot embeddedRoot)
  268. control.Arrange(new Rect(embeddedRoot.AllocatedSize));
  269. else if (control is ILayoutRoot root)
  270. control.Arrange(new Rect(root.DesiredSize));
  271. else if (control.PreviousArrange != null)
  272. {
  273. // Has been observed that PreviousArrange sometimes is null, probably a bug somewhere else.
  274. // Condition observed: control.VisualParent is Scrollbar, control is Border.
  275. control.Arrange(control.PreviousArrange.Value);
  276. }
  277. }
  278. }
  279. private void QueueLayoutPass()
  280. {
  281. if (!_queued && !_running)
  282. {
  283. _queued = true;
  284. Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout);
  285. }
  286. }
  287. private bool RaiseEffectiveViewportChanged()
  288. {
  289. var startCount = _toMeasure.Count + _toArrange.Count;
  290. if (_effectiveViewportChangedListeners is object)
  291. {
  292. var count = _effectiveViewportChangedListeners.Count;
  293. var pool = ArrayPool<EffectiveViewportChangedListener>.Shared;
  294. var listeners = pool.Rent(count);
  295. _effectiveViewportChangedListeners.CopyTo(listeners);
  296. try
  297. {
  298. for (var i = 0; i < count; ++i)
  299. {
  300. var l = _effectiveViewportChangedListeners[i];
  301. if (!l.Listener.IsAttachedToVisualTree)
  302. {
  303. continue;
  304. }
  305. var viewport = CalculateEffectiveViewport(l.Listener);
  306. if (viewport != l.Viewport)
  307. {
  308. l.Listener.EffectiveViewportChanged(new EffectiveViewportChangedEventArgs(viewport));
  309. _effectiveViewportChangedListeners[i] = new EffectiveViewportChangedListener(l.Listener, viewport);
  310. }
  311. }
  312. }
  313. finally
  314. {
  315. pool.Return(listeners, clearArray: true);
  316. }
  317. }
  318. return startCount != _toMeasure.Count + _toArrange.Count;
  319. }
  320. private Rect CalculateEffectiveViewport(IVisual control)
  321. {
  322. var viewport = new Rect(0, 0, double.PositiveInfinity, double.PositiveInfinity);
  323. CalculateEffectiveViewport(control, control, ref viewport);
  324. return viewport;
  325. }
  326. private void CalculateEffectiveViewport(IVisual target, IVisual control, ref Rect viewport)
  327. {
  328. // Recurse until the top level control.
  329. if (control.VisualParent is object)
  330. {
  331. CalculateEffectiveViewport(target, control.VisualParent, ref viewport);
  332. }
  333. else
  334. {
  335. viewport = new Rect(control.Bounds.Size);
  336. }
  337. // Apply the control clip bounds if it's not the target control. We don't apply it to
  338. // the target control because it may itself be clipped to bounds and if so the viewport
  339. // we calculate would be of no use.
  340. if (control != target && control.ClipToBounds)
  341. {
  342. viewport = control.Bounds.Intersect(viewport);
  343. }
  344. // Translate the viewport into this control's coordinate space.
  345. viewport = viewport.Translate(-control.Bounds.Position);
  346. if (control != target && control.RenderTransform is object)
  347. {
  348. var origin = control.RenderTransformOrigin.ToPixels(control.Bounds.Size);
  349. var offset = Matrix.CreateTranslation(origin);
  350. var renderTransform = (-offset) * control.RenderTransform.Value.Invert() * (offset);
  351. viewport = viewport.TransformToAABB(renderTransform);
  352. }
  353. }
  354. private readonly struct EffectiveViewportChangedListener
  355. {
  356. public EffectiveViewportChangedListener(ILayoutable listener, Rect viewport)
  357. {
  358. Listener = listener;
  359. Viewport = viewport;
  360. }
  361. public ILayoutable Listener { get; }
  362. public Rect Viewport { get; }
  363. }
  364. }
  365. }