PosAlign.cs 8.5 KB

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