Justification.cs 15 KB

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