PosAlign.cs 6.6 KB

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