PosAlign.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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.LayoutSubviews"/>
  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 class PosAlign : Pos
  24. {
  25. /// <summary>
  26. /// The cached location. Used to store the calculated location to minimize recalculating it.
  27. /// </summary>
  28. private int? _cachedLocation;
  29. /// <summary>
  30. /// Gets the identifier of a set of views that should be aligned together. When only a single
  31. /// set of views in a SuperView is aligned, setting <see cref="GroupId"/> is not needed because it defaults to 0.
  32. /// </summary>
  33. public int GroupId { get; init; }
  34. private readonly Aligner? _aligner;
  35. /// <summary>
  36. /// Gets the alignment settings.
  37. /// </summary>
  38. public required Aligner Aligner
  39. {
  40. get => _aligner!;
  41. init
  42. {
  43. if (_aligner is { })
  44. {
  45. _aligner.PropertyChanged -= Aligner_PropertyChanged;
  46. }
  47. _aligner = value;
  48. _aligner.PropertyChanged += Aligner_PropertyChanged;
  49. }
  50. }
  51. /// <summary>
  52. /// Aligns the views in <paramref name="views"/> that have the same group ID as <paramref name="groupId"/>.
  53. /// Updates each view's cached _location.
  54. /// </summary>
  55. /// <param name="groupId"></param>
  56. /// <param name="views"></param>
  57. /// <param name="dimension"></param>
  58. /// <param name="size"></param>
  59. private static void AlignAndUpdateGroup (int groupId, IList<View> views, Dimension dimension, int size)
  60. {
  61. List<int> dimensionsList = new ();
  62. // PERF: If this proves a perf issue, consider caching a ref to this list in each item
  63. List<View> viewsInGroup = views.Where (
  64. v =>
  65. {
  66. return dimension switch
  67. {
  68. Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId,
  69. Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId,
  70. _ => false
  71. };
  72. })
  73. .ToList ();
  74. if (viewsInGroup.Count == 0)
  75. {
  76. return;
  77. }
  78. // PERF: We iterate over viewsInGroup multiple times here.
  79. Aligner? firstInGroup = null;
  80. // Update the dimensionList with the sizes of the views
  81. for (var index = 0; index < viewsInGroup.Count; index++)
  82. {
  83. View view = viewsInGroup [index];
  84. PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
  85. if (posAlign is { })
  86. {
  87. if (index == 0)
  88. {
  89. firstInGroup = posAlign.Aligner;
  90. }
  91. dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
  92. }
  93. }
  94. // Update the first item in the group with the new container size.
  95. firstInGroup!.ContainerSize = size;
  96. // Align
  97. int [] locations = firstInGroup.Align (dimensionsList.ToArray ());
  98. // Update the cached location for each item
  99. for (var index = 0; index < viewsInGroup.Count; index++)
  100. {
  101. View view = viewsInGroup [index];
  102. PosAlign? align = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
  103. if (align is { })
  104. {
  105. align._cachedLocation = locations [index];
  106. }
  107. }
  108. }
  109. private void Aligner_PropertyChanged (object? sender, PropertyChangedEventArgs e) { _cachedLocation = null; }
  110. /// <inheritdoc/>
  111. public override bool Equals (object? other)
  112. {
  113. return other is PosAlign align
  114. && GroupId == align.GroupId
  115. && align.Aligner.Alignment == Aligner.Alignment
  116. && align.Aligner.AlignmentModes == Aligner.AlignmentModes;
  117. }
  118. /// <inheritdoc/>
  119. public override int GetHashCode () { return HashCode.Combine (Aligner, GroupId); }
  120. /// <inheritdoc/>
  121. public override string ToString () { return $"Align(alignment={Aligner.Alignment},modes={Aligner.AlignmentModes},groupId={GroupId})"; }
  122. internal override int GetAnchor (int width) { return _cachedLocation ?? 0 - width; }
  123. internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension)
  124. {
  125. if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension)
  126. {
  127. return _cachedLocation.Value;
  128. }
  129. if (us?.SuperView is null)
  130. {
  131. return 0;
  132. }
  133. AlignAndUpdateGroup (GroupId, us.SuperView.Subviews, dimension, superviewDimension);
  134. if (_cachedLocation.HasValue)
  135. {
  136. return _cachedLocation.Value;
  137. }
  138. return 0;
  139. }
  140. }