Justification.cs 12 KB

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