Aligner.cs 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. using System.ComponentModel;
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Aligns items within a container based on the specified <see cref="Gui.Alignment"/>. Both horizontal and vertical alignments are supported.
  5. /// </summary>
  6. public class Aligner : INotifyPropertyChanged
  7. {
  8. private Alignment _alignment;
  9. /// <summary>
  10. /// Gets or sets how the <see cref="Aligner"/> aligns items within a container.
  11. /// </summary>
  12. public Alignment Alignment
  13. {
  14. get => _alignment;
  15. set
  16. {
  17. _alignment = value;
  18. PropertyChanged?.Invoke (this, new (nameof (Alignment)));
  19. }
  20. }
  21. private int _containerSize;
  22. /// <summary>
  23. /// The size of the container.
  24. /// </summary>
  25. public int ContainerSize
  26. {
  27. get => _containerSize;
  28. set
  29. {
  30. _containerSize = value;
  31. PropertyChanged?.Invoke (this, new (nameof (ContainerSize)));
  32. }
  33. }
  34. private bool _spaceBetweenItems;
  35. /// <summary>
  36. /// Gets or sets whether <see cref="Aligner"/> adds at least one space between items. Default is
  37. /// <see langword="false"/>.
  38. /// </summary>
  39. /// <remarks>
  40. /// <para>
  41. /// If the total size of the items is greater than the container size, the space between items will be ignored
  42. /// starting from the right or bottom.
  43. /// </para>
  44. /// </remarks>
  45. public bool SpaceBetweenItems
  46. {
  47. get => _spaceBetweenItems;
  48. set
  49. {
  50. _spaceBetweenItems = value;
  51. PropertyChanged?.Invoke (this, new (nameof (SpaceBetweenItems)));
  52. }
  53. }
  54. /// <inheritdoc/>
  55. public event PropertyChangedEventHandler PropertyChanged;
  56. /// <summary>
  57. /// Takes a list of item sizes and returns a list of the positions of those items when aligned within <see name="ContainerSize"/>
  58. /// using the <see cref="Alignment"/> and <see cref="SpaceBetweenItems"/> settings.
  59. /// </summary>
  60. /// <param name="sizes">The sizes of the items to align.</param>
  61. /// <returns>The locations of the items, from left/top to right/bottom.</returns>
  62. public int [] Align (int [] sizes) { return Align (Alignment, SpaceBetweenItems, ContainerSize, sizes); }
  63. /// <summary>
  64. /// Takes a list of item sizes and returns a list of the positions of those items when aligned within <paramref name="containerSize"/>
  65. /// using specified parameters.
  66. /// </summary>
  67. /// <param name="sizes">The sizes of the items to align.</param>
  68. /// <param name="alignment">Specifies how the items will be aligned.</param>
  69. /// <param name="spaceBetweenItems">
  70. /// <para>
  71. /// Indicates whether at least one space should be added between items.
  72. /// </para>
  73. /// <para>
  74. /// If the total size of the items is greater than the container size, the space between items will be ignored
  75. /// starting from the right or bottom.
  76. /// </para>
  77. /// </param>
  78. /// <param name="containerSize">The size of the container.</param>
  79. /// <returns>The positions of the items, from left/top to right/bottom.</returns>
  80. public static int [] Align (Alignment alignment, bool spaceBetweenItems, int containerSize, int [] sizes)
  81. {
  82. if (sizes.Length == 0)
  83. {
  84. return new int [] { };
  85. }
  86. int maxSpaceBetweenItems = spaceBetweenItems ? 1 : 0;
  87. var positions = new int [sizes.Length]; // positions of the items. the return value.
  88. int totalItemsSize = sizes.Sum ();
  89. int totalGaps = sizes.Length - 1; // total gaps between items
  90. int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spaces if we had enough room
  91. int spaces = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out
  92. if (totalItemsSize >= containerSize)
  93. {
  94. spaces = 0;
  95. }
  96. else if (totalItemsAndSpaces > containerSize)
  97. {
  98. spaces = containerSize - totalItemsSize;
  99. }
  100. switch (alignment)
  101. {
  102. case Alignment.Start:
  103. var currentPosition = 0;
  104. for (var i = 0; i < sizes.Length; i++)
  105. {
  106. CheckSizeCannotBeNegative (i, sizes);
  107. if (i == 0)
  108. {
  109. positions [0] = 0; // first item position
  110. continue;
  111. }
  112. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  113. // subsequent items are placed one space after the previous item
  114. positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
  115. }
  116. break;
  117. case Alignment.End:
  118. currentPosition = containerSize - totalItemsSize - spaces;
  119. for (var i = 0; i < sizes.Length; i++)
  120. {
  121. CheckSizeCannotBeNegative (i, sizes);
  122. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  123. positions [i] = currentPosition;
  124. currentPosition += sizes [i] + spaceBefore;
  125. }
  126. break;
  127. case Alignment.Center:
  128. if (sizes.Length > 1)
  129. {
  130. // remaining space to be distributed before first and after the items
  131. int remainingSpace = Math.Max (0, containerSize - totalItemsSize - spaces);
  132. for (var i = 0; i < sizes.Length; i++)
  133. {
  134. CheckSizeCannotBeNegative (i, sizes);
  135. if (i == 0)
  136. {
  137. positions [i] = remainingSpace / 2; // first item position
  138. continue;
  139. }
  140. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  141. // subsequent items are placed one space after the previous item
  142. positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
  143. }
  144. }
  145. else if (sizes.Length == 1)
  146. {
  147. CheckSizeCannotBeNegative (0, sizes);
  148. positions [0] = (containerSize - sizes [0]) / 2; // single item is centered
  149. }
  150. break;
  151. case Alignment.Fill:
  152. int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0;
  153. int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0;
  154. currentPosition = 0;
  155. for (var i = 0; i < sizes.Length; i++)
  156. {
  157. CheckSizeCannotBeNegative (i, sizes);
  158. positions [i] = currentPosition;
  159. int extraSpace = i < remainder ? 1 : 0;
  160. currentPosition += sizes [i] + spaceBetween + extraSpace;
  161. }
  162. break;
  163. // 111 2222 33333
  164. case Alignment.LastEndRestStart:
  165. if (sizes.Length > 1)
  166. {
  167. if (totalItemsSize > containerSize)
  168. {
  169. currentPosition = containerSize - totalItemsSize - spaces;
  170. }
  171. else
  172. {
  173. currentPosition = 0;
  174. }
  175. for (var i = 0; i < sizes.Length; i++)
  176. {
  177. CheckSizeCannotBeNegative (i, sizes);
  178. if (i < sizes.Length - 1)
  179. {
  180. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  181. positions [i] = currentPosition;
  182. currentPosition += sizes [i] + spaceBefore;
  183. }
  184. }
  185. positions [sizes.Length - 1] = containerSize - sizes [^1];
  186. }
  187. else if (sizes.Length == 1)
  188. {
  189. CheckSizeCannotBeNegative (0, sizes);
  190. positions [0] = containerSize - sizes [0]; // single item is flush right
  191. }
  192. break;
  193. // 111 2222 33333
  194. case Alignment.FirstStartRestEnd:
  195. if (sizes.Length > 1)
  196. {
  197. currentPosition = 0;
  198. positions [0] = currentPosition; // first item is flush left
  199. for (int i = sizes.Length - 1; i >= 0; i--)
  200. {
  201. CheckSizeCannotBeNegative (i, sizes);
  202. if (i == sizes.Length - 1)
  203. {
  204. // start at right
  205. currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i];
  206. positions [i] = currentPosition;
  207. }
  208. if (i < sizes.Length - 1 && i > 0)
  209. {
  210. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  211. positions [i] = currentPosition - sizes [i] - spaceBefore;
  212. currentPosition = positions [i];
  213. }
  214. }
  215. }
  216. else if (sizes.Length == 1)
  217. {
  218. CheckSizeCannotBeNegative (0, sizes);
  219. positions [0] = 0; // single item is flush left
  220. }
  221. break;
  222. default:
  223. throw new ArgumentOutOfRangeException (nameof (alignment), alignment, null);
  224. }
  225. return positions;
  226. }
  227. private static void CheckSizeCannotBeNegative (int i, int [] sizes)
  228. {
  229. if (sizes [i] < 0)
  230. {
  231. throw new ArgumentException ("The size of an item cannot be negative.");
  232. }
  233. }
  234. }