Aligner.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. using System.ComponentModel;
  2. using Microsoft.CodeAnalysis;
  3. using static Terminal.Gui.Pos;
  4. namespace Terminal.Gui;
  5. /// <summary>
  6. /// Controls how the <see cref="Aligner"/> aligns items within a container.
  7. /// </summary>
  8. public enum Alignment
  9. {
  10. /// <summary>
  11. /// The items will be aligned to the left.
  12. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
  13. /// each item.
  14. /// </summary>
  15. /// <remarks>
  16. /// <para>
  17. /// If the container is smaller than the total size of the items, the right items will be clipped (their locations
  18. /// will be greater than the container size).
  19. /// </para>
  20. /// </remarks>
  21. /// <example>
  22. /// <c>
  23. /// 111 2222 33333
  24. /// </c>
  25. /// </example>
  26. Left,
  27. /// <summary>
  28. /// The items will be aligned to the top.
  29. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one line between
  30. /// each item.
  31. /// </summary>
  32. Top,
  33. /// <summary>
  34. /// The items will be aligned to the right.
  35. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
  36. /// each item.
  37. /// </summary>
  38. /// <remarks>
  39. /// <para>
  40. /// If the container is smaller than the total size of the items, the left items will be clipped (their locations
  41. /// will be negative).
  42. /// </para>
  43. /// </remarks>
  44. /// <example>
  45. /// <c>
  46. /// 111 2222 33333
  47. /// </c>
  48. /// </example>
  49. Right,
  50. /// <summary>
  51. /// The items will be aligned to the bottom.
  52. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one line between
  53. /// each item.
  54. /// </summary>
  55. Bottom,
  56. /// <summary>
  57. /// The group will be centered in the container.
  58. /// If centering is not possible, the group will be left-aligned.
  59. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
  60. /// each item.
  61. /// </summary>
  62. /// <remarks>
  63. /// <para>
  64. /// Extra space will be distributed between the items, biased towards the left.
  65. /// </para>
  66. /// </remarks>
  67. /// <example>
  68. /// <c>
  69. /// 111 2222 33333
  70. /// </c>
  71. /// </example>
  72. Centered,
  73. /// <summary>
  74. /// The items will be justified. Space will be added between the items such that the first item
  75. /// is at the start and the right side of the last item against the end.
  76. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
  77. /// each item.
  78. /// </summary>
  79. /// <remarks>
  80. /// <para>
  81. /// Extra space will be distributed between the items, biased towards the left.
  82. /// </para>
  83. /// </remarks>
  84. /// <example>
  85. /// <c>
  86. /// 111 2222 33333
  87. /// </c>
  88. /// </example>
  89. Justified,
  90. /// <summary>
  91. /// The first item will be aligned to the left and the remaining will aligned to the right.
  92. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
  93. /// each item.
  94. /// </summary>
  95. /// <remarks>
  96. /// <para>
  97. /// If the container is smaller than the total size of the items, the right items will be clipped (their locations
  98. /// will be greater than the container size).
  99. /// </para>
  100. /// </remarks>
  101. /// <example>
  102. /// <c>
  103. /// 111 2222 33333
  104. /// </c>
  105. /// </example>
  106. FirstLeftRestRight,
  107. /// <summary>
  108. /// The first item will be aligned to the top and the remaining will aligned to the bottom.
  109. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one line between
  110. /// each item.
  111. /// </summary>
  112. FirstTopRestBottom,
  113. /// <summary>
  114. /// The last item will be aligned to the right and the remaining will aligned to the left.
  115. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one space between
  116. /// each item.
  117. /// </summary>
  118. /// <remarks>
  119. /// <para>
  120. /// If the container is smaller than the total size of the items, the left items will be clipped (their locations
  121. /// will be negative).
  122. /// </para>
  123. /// </remarks>
  124. /// <example>
  125. /// <c>
  126. /// 111 2222 33333
  127. /// </c>
  128. /// </example>
  129. LastRightRestLeft,
  130. /// <summary>
  131. /// The last item will be aligned to the bottom and the remaining will aligned to the left.
  132. /// Set <see cref="Aligner.PutSpaceBetweenItems"/> to <see langword="true"/> to ensure at least one line between
  133. /// each item.
  134. /// </summary>
  135. LastBottomRestTop
  136. }
  137. /// <summary>
  138. /// Aligns items within a container based on the specified <see cref="Gui.Alignment"/>.
  139. /// </summary>
  140. public class Aligner : INotifyPropertyChanged
  141. {
  142. private Alignment _alignment;
  143. /// <summary>
  144. /// Gets or sets how the <see cref="Aligner"/> aligns items within a container.
  145. /// </summary>
  146. public Alignment Alignment
  147. {
  148. get => _alignment;
  149. set
  150. {
  151. _alignment = value;
  152. PropertyChanged?.Invoke (this, new (nameof (Alignment)));
  153. }
  154. }
  155. private int _containerSize;
  156. /// <summary>
  157. /// The size of the container.
  158. /// </summary>
  159. public int ContainerSize
  160. {
  161. get => _containerSize;
  162. set
  163. {
  164. _containerSize = value;
  165. PropertyChanged?.Invoke (this, new (nameof (ContainerSize)));
  166. }
  167. }
  168. private bool _putSpaceBetweenItems;
  169. /// <summary>
  170. /// Gets or sets whether <see cref="Aligner"/> puts a space is placed between items. Default is
  171. /// <see langword="false"/>. If <see langword="true"/>, a space will be
  172. /// placed between each item, which is useful for justifying text.
  173. /// </summary>
  174. /// <remarks>
  175. /// <para>
  176. /// If the total size of the items is greater than the container size, the space between items will be ignored
  177. /// starting
  178. /// from the right.
  179. /// </para>
  180. /// </remarks>
  181. public bool PutSpaceBetweenItems
  182. {
  183. get => _putSpaceBetweenItems;
  184. set
  185. {
  186. _putSpaceBetweenItems = value;
  187. PropertyChanged?.Invoke (this, new (nameof (PutSpaceBetweenItems)));
  188. }
  189. }
  190. /// <inheritdoc/>
  191. public event PropertyChangedEventHandler PropertyChanged;
  192. /// <summary>
  193. /// Takes a list of items and returns their positions when aligned within a container <see name="ContainerSize"/>
  194. /// wide based on the specified
  195. /// <see cref="Alignment"/>.
  196. /// </summary>
  197. /// <param name="sizes">The sizes of the items to align.</param>
  198. /// <returns>The locations of the items, from left to right.</returns>
  199. public int [] Align (int [] sizes) { return Align (Alignment, PutSpaceBetweenItems, ContainerSize, sizes); }
  200. /// <summary>
  201. /// Takes a list of items and returns their positions when aligned within a container
  202. /// <paramref name="containerSize"/> wide based on the specified
  203. /// <see cref="Alignment"/>.
  204. /// </summary>
  205. /// <param name="sizes">The sizes of the items to align.</param>
  206. /// <param name="alignment">Specifies how the items will be aligned.</param>
  207. /// <param name="putSpaceBetweenItems">Puts a space is placed between items.</param>
  208. /// <param name="containerSize">The size of the container.</param>
  209. /// <returns>The locations of the items, from left to right.</returns>
  210. public static int [] Align (Alignment alignment, bool putSpaceBetweenItems, int containerSize, int [] sizes)
  211. {
  212. if (sizes.Length == 0)
  213. {
  214. return new int [] { };
  215. }
  216. int maxSpaceBetweenItems = putSpaceBetweenItems ? 1 : 0;
  217. var positions = new int [sizes.Length]; // positions of the items. the return value.
  218. int totalItemsSize = sizes.Sum ();
  219. int totalGaps = sizes.Length - 1; // total gaps between items
  220. int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spaces if we had enough room
  221. int spaces = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out
  222. if (totalItemsSize >= containerSize)
  223. {
  224. spaces = 0;
  225. }
  226. else if (totalItemsAndSpaces > containerSize)
  227. {
  228. spaces = containerSize - totalItemsSize;
  229. }
  230. switch (alignment)
  231. {
  232. case Alignment.Left:
  233. case Alignment.Top:
  234. var currentPosition = 0;
  235. for (var i = 0; i < sizes.Length; i++)
  236. {
  237. CheckSizeCannotBeNegative (i, sizes);
  238. if (i == 0)
  239. {
  240. positions [0] = 0; // first item position
  241. continue;
  242. }
  243. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  244. // subsequent items are placed one space after the previous item
  245. positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore;
  246. }
  247. break;
  248. case Alignment.Right:
  249. case Alignment.Bottom:
  250. currentPosition = containerSize - totalItemsSize - spaces;
  251. for (var i = 0; i < sizes.Length; i++)
  252. {
  253. CheckSizeCannotBeNegative (i, sizes);
  254. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  255. positions [i] = currentPosition;
  256. currentPosition += sizes [i] + spaceBefore;
  257. }
  258. break;
  259. case Alignment.Centered:
  260. if (sizes.Length > 1)
  261. {
  262. // remaining space to be distributed before first and after the items
  263. int remainingSpace = Math.Max (0, containerSize - totalItemsSize - spaces);
  264. for (var i = 0; i < sizes.Length; i++)
  265. {
  266. CheckSizeCannotBeNegative (i, sizes);
  267. if (i == 0)
  268. {
  269. positions [i] = remainingSpace / 2; // first item position
  270. continue;
  271. }
  272. int spaceBefore = spaces-- > 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, sizes);
  280. positions [0] = (containerSize - sizes [0]) / 2; // single item is centered
  281. }
  282. break;
  283. case Alignment.Justified:
  284. int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0;
  285. int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0;
  286. currentPosition = 0;
  287. for (var i = 0; i < sizes.Length; i++)
  288. {
  289. CheckSizeCannotBeNegative (i, sizes);
  290. positions [i] = currentPosition;
  291. int extraSpace = i < remainder ? 1 : 0;
  292. currentPosition += sizes [i] + spaceBetween + extraSpace;
  293. }
  294. break;
  295. // 111 2222 33333
  296. case Alignment.LastRightRestLeft:
  297. case Alignment.LastBottomRestTop:
  298. if (sizes.Length > 1)
  299. {
  300. if (totalItemsSize > containerSize)
  301. {
  302. currentPosition = containerSize - totalItemsSize - spaces;
  303. }
  304. else
  305. {
  306. currentPosition = 0;
  307. }
  308. for (var i = 0; i < sizes.Length; i++)
  309. {
  310. CheckSizeCannotBeNegative (i, sizes);
  311. if (i < sizes.Length - 1)
  312. {
  313. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  314. positions [i] = currentPosition;
  315. currentPosition += sizes [i] + spaceBefore;
  316. }
  317. }
  318. positions [sizes.Length - 1] = containerSize - sizes [^1];
  319. }
  320. else if (sizes.Length == 1)
  321. {
  322. CheckSizeCannotBeNegative (0, sizes);
  323. positions [0] = containerSize - sizes [0]; // single item is flush right
  324. }
  325. break;
  326. // 111 2222 33333
  327. case Alignment.FirstLeftRestRight:
  328. case Alignment.FirstTopRestBottom:
  329. if (sizes.Length > 1)
  330. {
  331. currentPosition = 0;
  332. positions [0] = currentPosition; // first item is flush left
  333. for (int i = sizes.Length - 1; i >= 0; i--)
  334. {
  335. CheckSizeCannotBeNegative (i, sizes);
  336. if (i == sizes.Length - 1)
  337. {
  338. // start at right
  339. currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i];
  340. positions [i] = currentPosition;
  341. }
  342. if (i < sizes.Length - 1 && i > 0)
  343. {
  344. int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0;
  345. positions [i] = currentPosition - sizes [i] - spaceBefore;
  346. currentPosition = positions [i];
  347. }
  348. }
  349. }
  350. else if (sizes.Length == 1)
  351. {
  352. CheckSizeCannotBeNegative (0, sizes);
  353. positions [0] = 0; // single item is flush left
  354. }
  355. break;
  356. default:
  357. throw new ArgumentOutOfRangeException (nameof (alignment), alignment, null);
  358. }
  359. return positions;
  360. }
  361. private static void CheckSizeCannotBeNegative (int i, int [] sizes)
  362. {
  363. if (sizes [i] < 0)
  364. {
  365. throw new ArgumentException ("The size of an item cannot be negative.");
  366. }
  367. }
  368. }