Aligner.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. #nullable disable
  2. using System.ComponentModel;
  3. namespace Terminal.Gui.ViewBase;
  4. /// <summary>
  5. /// Aligns items within a container based on the specified <see cref="Alignment"/>. Both horizontal and vertical
  6. /// alignments are supported.
  7. /// </summary>
  8. public class Aligner : INotifyPropertyChanged
  9. {
  10. private Alignment _alignment;
  11. /// <summary>
  12. /// Gets or sets how the <see cref="Aligner"/> aligns items within a container.
  13. /// </summary>
  14. /// <remarks>
  15. /// <para>
  16. /// <see cref="AlignmentModes"/> provides additional options for aligning items in a container.
  17. /// </para>
  18. /// </remarks>
  19. public Alignment Alignment
  20. {
  21. get => _alignment;
  22. set
  23. {
  24. _alignment = value;
  25. PropertyChanged?.Invoke (this, new (nameof (Alignment)));
  26. }
  27. }
  28. private AlignmentModes _alignmentMode = AlignmentModes.StartToEnd;
  29. /// <summary>
  30. /// Gets or sets the modes controlling <see cref="Alignment"/>.
  31. /// </summary>
  32. public AlignmentModes AlignmentModes
  33. {
  34. get => _alignmentMode;
  35. set
  36. {
  37. _alignmentMode = value;
  38. PropertyChanged?.Invoke (this, new (nameof (AlignmentModes)));
  39. }
  40. }
  41. private int _containerSize;
  42. /// <summary>
  43. /// The size of the container.
  44. /// </summary>
  45. public int ContainerSize
  46. {
  47. get => _containerSize;
  48. set
  49. {
  50. _containerSize = value;
  51. PropertyChanged?.Invoke (this, new (nameof (ContainerSize)));
  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
  58. /// <see name="ContainerSize"/>
  59. /// using the <see cref="Alignment"/> and <see cref="AlignmentModes"/> settings.
  60. /// </summary>
  61. /// <param name="sizes">The sizes of the items to align.</param>
  62. /// <returns>The locations of the items, from left/top to right/bottom.</returns>
  63. public int [] Align (int [] sizes) { return Align (Alignment, AlignmentModes, ContainerSize, sizes); }
  64. /// <summary>
  65. /// Takes a list of item sizes and returns a list of the positions of those items when aligned within
  66. /// <paramref name="containerSize"/>
  67. /// using specified parameters.
  68. /// </summary>
  69. /// <param name="alignment">Specifies how the items will be aligned.</param>
  70. /// <param name="alignmentMode"></param>
  71. /// <param name="containerSize">The size of the container.</param>
  72. /// <param name="sizes">The sizes of the items to align.</param>
  73. /// <returns>The positions of the items, from left/top to right/bottom.</returns>
  74. public static int [] Align (in Alignment alignment, in AlignmentModes alignmentMode, in int containerSize, in int [] sizes)
  75. {
  76. if (sizes.Length == 0)
  77. {
  78. return [];
  79. }
  80. var sizesCopy = sizes;
  81. if (alignmentMode.FastHasFlags (AlignmentModes.EndToStart))
  82. {
  83. sizesCopy = sizes.Reverse ().ToArray ();
  84. }
  85. int maxSpaceBetweenItems = alignmentMode.FastHasFlags (AlignmentModes.AddSpaceBetweenItems) ? 1 : 0;
  86. int totalItemsSize = sizes.Sum ();
  87. int totalGaps = sizes.Length - 1; // total gaps between items
  88. int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spacesToGive if we had enough room
  89. int spacesToGive = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out
  90. if (totalItemsSize >= containerSize)
  91. {
  92. spacesToGive = 0;
  93. }
  94. else if (totalItemsAndSpaces > containerSize)
  95. {
  96. spacesToGive = containerSize - totalItemsSize;
  97. }
  98. AlignmentModes mode = alignmentMode & ~AlignmentModes.AddSpaceBetweenItems; // copy to avoid modifying the original
  99. switch (alignment)
  100. {
  101. case Alignment.Start:
  102. switch (mode)
  103. {
  104. case AlignmentModes.StartToEnd:
  105. return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive);
  106. case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast:
  107. return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
  108. case AlignmentModes.EndToStart:
  109. return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray ();
  110. case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast:
  111. return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ;
  112. }
  113. break;
  114. case Alignment.End:
  115. switch (mode)
  116. {
  117. case AlignmentModes.StartToEnd:
  118. return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
  119. case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast:
  120. return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
  121. case AlignmentModes.EndToStart:
  122. return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray ();
  123. case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast:
  124. return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ;
  125. }
  126. break;
  127. case Alignment.Center:
  128. mode &= ~AlignmentModes.IgnoreFirstOrLast;
  129. switch (mode)
  130. {
  131. case AlignmentModes.StartToEnd:
  132. return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive);
  133. case AlignmentModes.EndToStart:
  134. return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray ();
  135. }
  136. break;
  137. case Alignment.Fill:
  138. mode &= ~AlignmentModes.IgnoreFirstOrLast;
  139. switch (mode)
  140. {
  141. case AlignmentModes.StartToEnd:
  142. return Fill (in sizesCopy, containerSize, totalItemsSize);
  143. case AlignmentModes.EndToStart:
  144. return Fill (in sizesCopy, containerSize, totalItemsSize).Reverse ().ToArray ();
  145. }
  146. break;
  147. default:
  148. throw new ArgumentOutOfRangeException (nameof (alignment), alignment, null);
  149. }
  150. return [];
  151. }
  152. internal static int [] Start (ref readonly int [] sizes, int maxSpaceBetweenItems, int spacesToGive)
  153. {
  154. var positions = new int [sizes.Length]; // positions of the items. the return value.
  155. for (var i = 0; i < sizes.Length; i++)
  156. {
  157. CheckSizeCannotBeNegative (i, in sizes);
  158. if (i == 0)
  159. {
  160. positions [0] = 0; // first item position
  161. continue;
  162. }
  163. int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
  164. // subsequent items are placed one space after the previous item
  165. positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
  166. }
  167. return positions;
  168. }
  169. internal static int [] IgnoreFirst (
  170. ref readonly int [] sizes,
  171. int containerSize,
  172. int totalItemsSize,
  173. int maxSpaceBetweenItems,
  174. int spacesToGive
  175. )
  176. {
  177. var positions = new int [sizes.Length]; // positions of the items. the return value.
  178. if (sizes.Length > 1)
  179. {
  180. var currentPosition = 0;
  181. positions [0] = currentPosition; // first item is flush left
  182. for (int i = sizes.Length - 1; i >= 0; i--)
  183. {
  184. CheckSizeCannotBeNegative (i, in sizes);
  185. if (i == sizes.Length - 1)
  186. {
  187. // start at right
  188. currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i];
  189. positions [i] = currentPosition;
  190. }
  191. if (i < sizes.Length - 1 && i > 0)
  192. {
  193. int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
  194. positions [i] = currentPosition - sizes [i] - spaceBefore;
  195. currentPosition = positions [i];
  196. }
  197. }
  198. }
  199. else if (sizes.Length == 1)
  200. {
  201. CheckSizeCannotBeNegative (0, in sizes);
  202. positions [0] = 0; // single item is flush left
  203. }
  204. return positions;
  205. }
  206. internal static int [] IgnoreLast (
  207. ref readonly int [] sizes,
  208. int containerSize,
  209. int totalItemsSize,
  210. int maxSpaceBetweenItems,
  211. int spacesToGive
  212. )
  213. {
  214. var positions = new int [sizes.Length]; // positions of the items. the return value.
  215. if (sizes.Length > 1)
  216. {
  217. var currentPosition = 0;
  218. if (totalItemsSize > containerSize)
  219. {
  220. // Don't allow negative positions
  221. currentPosition = int.Max(0, containerSize - totalItemsSize - spacesToGive);
  222. }
  223. for (var i = 0; i < sizes.Length; i++)
  224. {
  225. CheckSizeCannotBeNegative (i, in sizes);
  226. if (i < sizes.Length - 1)
  227. {
  228. int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
  229. positions [i] = currentPosition;
  230. currentPosition += sizes [i] + spaceBefore;
  231. }
  232. }
  233. positions [sizes.Length - 1] = containerSize - sizes [^1];
  234. }
  235. else if (sizes.Length == 1)
  236. {
  237. CheckSizeCannotBeNegative (0, in sizes);
  238. positions [0] = containerSize - sizes [0]; // single item is flush right
  239. }
  240. return positions;
  241. }
  242. internal static int [] Fill (ref readonly int [] sizes, int containerSize, int totalItemsSize)
  243. {
  244. var positions = new int [sizes.Length]; // positions of the items. the return value.
  245. int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0;
  246. int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0;
  247. var currentPosition = 0;
  248. for (var i = 0; i < sizes.Length; i++)
  249. {
  250. CheckSizeCannotBeNegative (i, in sizes);
  251. positions [i] = currentPosition;
  252. int extraSpace = i < remainder ? 1 : 0;
  253. currentPosition += sizes [i] + spaceBetween + extraSpace;
  254. }
  255. return positions;
  256. }
  257. internal static int [] Center (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive)
  258. {
  259. var positions = new int [sizes.Length]; // positions of the items. the return value.
  260. if (sizes.Length > 1)
  261. {
  262. // remaining space to be distributed before first and after the items
  263. int remainingSpace = containerSize - totalItemsSize - spacesToGive;
  264. for (var i = 0; i < sizes.Length; i++)
  265. {
  266. CheckSizeCannotBeNegative (i, in sizes);
  267. if (i == 0)
  268. {
  269. positions [i] = remainingSpace / 2; // first item position
  270. continue;
  271. }
  272. int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
  273. // subsequent items are placed one space after the previous item
  274. positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
  275. }
  276. }
  277. else if (sizes.Length == 1)
  278. {
  279. CheckSizeCannotBeNegative (0, in sizes);
  280. positions [0] = (containerSize - sizes [0]) / 2; // single item is centered
  281. }
  282. return positions;
  283. }
  284. internal static int [] End (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive)
  285. {
  286. var positions = new int [sizes.Length]; // positions of the items. the return value.
  287. int currentPosition = containerSize - totalItemsSize - spacesToGive;
  288. for (var i = 0; i < sizes.Length; i++)
  289. {
  290. CheckSizeCannotBeNegative (i, in sizes);
  291. int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0;
  292. positions [i] = currentPosition;
  293. currentPosition += sizes [i] + spaceBefore;
  294. }
  295. return positions;
  296. }
  297. private static void CheckSizeCannotBeNegative (int i, ref readonly int [] sizes)
  298. {
  299. if (sizes [i] < 0)
  300. {
  301. throw new ArgumentException ("The size of an item cannot be negative.");
  302. }
  303. }
  304. }