Aligner.cs 17 KB

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