Justification.cs 13 KB

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