Aligner.cs 13 KB

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