PosAlign.cs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. #nullable enable
  2. using System.ComponentModel;
  3. namespace Terminal.Gui;
  4. /// <summary>
  5. /// Enables alignment of a set of views.
  6. /// </summary>
  7. /// <remarks>
  8. /// <para>
  9. /// Updating the properties of <see cref="Aligner"/> is supported, but will not automatically cause re-layout to
  10. /// happen. <see cref="View.Layout()"/>
  11. /// must be called on the SuperView.
  12. /// </para>
  13. /// <para>
  14. /// Views that should be aligned together must have a distinct <see cref="GroupId"/>. When only a single
  15. /// set of views is aligned within a SuperView, setting <see cref="GroupId"/> is optional because it defaults to 0.
  16. /// </para>
  17. /// <para>
  18. /// The first view added to the Superview with a given <see cref="GroupId"/> is used to determine the alignment of
  19. /// the group.
  20. /// The alignment is applied to all views with the same <see cref="GroupId"/>.
  21. /// </para>
  22. /// </remarks>
  23. public record PosAlign : Pos
  24. {
  25. /// <summary>
  26. /// The cached location. Used to store the calculated location to minimize recalculating it.
  27. /// </summary>
  28. public int? _cachedLocation;
  29. private readonly Aligner? _aligner;
  30. /// <summary>
  31. /// Gets the alignment settings.
  32. /// </summary>
  33. public required Aligner Aligner
  34. {
  35. get => _aligner!;
  36. init
  37. {
  38. if (_aligner is { })
  39. {
  40. _aligner.PropertyChanged -= Aligner_PropertyChanged;
  41. }
  42. _aligner = value;
  43. _aligner.PropertyChanged += Aligner_PropertyChanged;
  44. }
  45. }
  46. // TODO: PosAlign.CalculateMinDimension is a hack. Need to figure out a better way of doing this.
  47. /// <summary>
  48. /// Returns the minimum size a group of views with the same <paramref name="groupId"/> can be.
  49. /// </summary>
  50. /// <param name="groupId"></param>
  51. /// <param name="views"></param>
  52. /// <param name="dimension"></param>
  53. /// <returns></returns>
  54. public static int CalculateMinDimension (int groupId, IList<View> views, Dimension dimension)
  55. {
  56. List<int> dimensionsList = new ();
  57. // PERF: If this proves a perf issue, consider caching a ref to this list in each item
  58. List<View> viewsInGroup = views.Where (
  59. v =>
  60. {
  61. return dimension switch
  62. {
  63. Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
  64. Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
  65. _ => false
  66. };
  67. })
  68. .ToList ();
  69. if (viewsInGroup.Count == 0)
  70. {
  71. return 0;
  72. }
  73. // PERF: We iterate over viewsInGroup multiple times here.
  74. // Update the dimensionList with the sizes of the views
  75. for (var index = 0; index < viewsInGroup.Count; index++)
  76. {
  77. View view = viewsInGroup [index];
  78. PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
  79. if (posAlign is { })
  80. {
  81. dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
  82. }
  83. }
  84. // Align
  85. return dimensionsList.Sum ();
  86. }
  87. /// <summary>
  88. /// Gets the identifier of a set of views that should be aligned together. When only a single
  89. /// set of views in a SuperView is aligned, setting <see cref="GroupId"/> is not needed because it defaults to 0.
  90. /// </summary>
  91. public int GroupId { get; init; }
  92. /// <inheritdoc/>
  93. public override string ToString () { return $"Align(alignment={Aligner.Alignment},modes={Aligner.AlignmentModes},groupId={GroupId})"; }
  94. internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension)
  95. {
  96. if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension && !us.IsLayoutNeeded())
  97. {
  98. return _cachedLocation.Value;
  99. }
  100. if (us.SuperView is null)
  101. {
  102. return 0;
  103. }
  104. AlignAndUpdateGroup (GroupId, us.SuperView.Subviews, dimension, superviewDimension);
  105. if (_cachedLocation.HasValue)
  106. {
  107. return _cachedLocation.Value;
  108. }
  109. return 0;
  110. }
  111. internal override int GetAnchor (int width) { return _cachedLocation ?? 0 - width; }
  112. /// <summary>
  113. /// Aligns the views in <paramref name="views"/> that have the same group ID as <paramref name="groupId"/>.
  114. /// Updates each view's cached _location.
  115. /// </summary>
  116. /// <param name="groupId"></param>
  117. /// <param name="views"></param>
  118. /// <param name="dimension"></param>
  119. /// <param name="size"></param>
  120. private static void AlignAndUpdateGroup (int groupId, IList<View> views, Dimension dimension, int size)
  121. {
  122. List<int> dimensionsList = new ();
  123. // PERF: If this proves a perf issue, consider caching a ref to this list in each item
  124. List<PosAlign?> posAligns = views.Select (
  125. v =>
  126. {
  127. switch (dimension)
  128. {
  129. case Dimension.Width when v.X.Has<PosAlign> (out Pos pos):
  130. if (pos is PosAlign posAlignX && posAlignX.GroupId == groupId)
  131. {
  132. return posAlignX;
  133. }
  134. break;
  135. case Dimension.Height when v.Y.Has<PosAlign> (out Pos pos):
  136. if (pos is PosAlign posAlignY && posAlignY.GroupId == groupId)
  137. {
  138. return posAlignY;
  139. }
  140. break;
  141. }
  142. return null;
  143. })
  144. .ToList ();
  145. // PERF: We iterate over viewsInGroup multiple times here.
  146. Aligner? firstInGroup = null;
  147. // Update the dimensionList with the sizes of the views
  148. for (var index = 0; index < posAligns.Count; index++)
  149. {
  150. if (posAligns [index] is { })
  151. {
  152. if (firstInGroup is null)
  153. {
  154. firstInGroup = posAligns [index]!.Aligner;
  155. }
  156. dimensionsList.Add (dimension == Dimension.Width ? views [index].Frame.Width : views [index].Frame.Height);
  157. }
  158. }
  159. if (firstInGroup is null)
  160. {
  161. return;
  162. }
  163. // Update the first item in the group with the new container size.
  164. firstInGroup.ContainerSize = size;
  165. // Align
  166. int [] locations = firstInGroup.Align (dimensionsList.ToArray ());
  167. // Update the cached location for each item
  168. for (int posIndex = 0, locIndex = 0; posIndex < posAligns.Count; posIndex++)
  169. {
  170. if (posAligns [posIndex] is { })
  171. {
  172. posAligns [posIndex]!._cachedLocation = locations [locIndex++];
  173. }
  174. }
  175. }
  176. private void Aligner_PropertyChanged (object? sender, PropertyChangedEventArgs e) { _cachedLocation = null; }
  177. }