Table.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using QuestPDF.Drawing;
  5. using QuestPDF.Infrastructure;
  6. namespace QuestPDF.Elements.Table
  7. {
  8. internal sealed class Table : Element, IStateResettable, IContentDirectionAware
  9. {
  10. public ContentDirection ContentDirection { get; set; }
  11. public List<TableColumnDefinition> Columns { get; set; } = new();
  12. public List<TableCell> Cells { get; set; } = new();
  13. public bool ExtendLastCellsToTableBottom { get; set; }
  14. private bool CacheInitialized { get; set; }
  15. private bool IsRendered => CurrentRow > StartingRowsCount;
  16. private int StartingRowsCount { get; set; }
  17. private int RowsCount { get; set; }
  18. private int CurrentRow { get; set; }
  19. // cache that stores all cells
  20. // first index: row number
  21. // inner table: list of all cells that ends at the corresponding row
  22. private TableCell[][] CellsCache { get; set; }
  23. private int MaxRow { get; set; }
  24. private int MaxRowSpan { get; set; }
  25. internal override IEnumerable<Element?> GetChildren()
  26. {
  27. return Cells;
  28. }
  29. public void ResetState(bool hardReset)
  30. {
  31. Initialize();
  32. foreach (var x in Cells)
  33. x.IsRendered = false;
  34. CurrentRow = 1;
  35. }
  36. private void Initialize()
  37. {
  38. if (CacheInitialized)
  39. return;
  40. StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
  41. RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
  42. Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
  43. BuildCache();
  44. CacheInitialized = true;
  45. }
  46. private void BuildCache()
  47. {
  48. if (CellsCache != null)
  49. return;
  50. if (Cells.Count == 0)
  51. {
  52. MaxRow = 0;
  53. MaxRowSpan = 1;
  54. CellsCache = Array.Empty<TableCell[]>();
  55. return;
  56. }
  57. var groups = Cells
  58. .GroupBy(x => x.Row + x.RowSpan - 1)
  59. .ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray());
  60. MaxRow = groups.Max(x => x.Key);
  61. MaxRowSpan = Cells.Max(x => x.RowSpan);
  62. CellsCache = Enumerable
  63. .Range(0, MaxRow + 1)
  64. .Select(x => groups.TryGetValue(x, out var value) ? value : Array.Empty<TableCell>())
  65. .ToArray();
  66. }
  67. internal override SpacePlan Measure(Size availableSpace)
  68. {
  69. if (!Cells.Any())
  70. return SpacePlan.Empty();
  71. if (IsRendered)
  72. return SpacePlan.Empty();
  73. UpdateColumnsWidth(availableSpace.Width);
  74. var renderingCommands = PlanLayout(availableSpace);
  75. if (!renderingCommands.Any())
  76. return SpacePlan.Wrap();
  77. var width = Columns.Sum(x => x.Width);
  78. var height = renderingCommands.Max(x => x.Offset.Y + x.Size.Height);
  79. var tableSize = new Size(width, height);
  80. if (tableSize.Width > availableSpace.Width + Size.Epsilon)
  81. return SpacePlan.Wrap();
  82. return CalculateCurrentRow(renderingCommands) > StartingRowsCount
  83. ? SpacePlan.FullRender(tableSize)
  84. : SpacePlan.PartialRender(tableSize);
  85. }
  86. internal override void Draw(Size availableSpace)
  87. {
  88. if (IsRendered)
  89. return;
  90. UpdateColumnsWidth(availableSpace.Width);
  91. var renderingCommands = PlanLayout(availableSpace);
  92. foreach (var command in renderingCommands.OrderBy(x => x.Cell.ZIndex))
  93. {
  94. if (command.Measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender)
  95. command.Cell.IsRendered = true;
  96. if (command.Measurement.Type == SpacePlanType.Wrap)
  97. continue;
  98. var offset = ContentDirection == ContentDirection.LeftToRight
  99. ? command.Offset
  100. : new Position(availableSpace.Width - command.Offset.X - command.Size.Width, command.Offset.Y);
  101. Canvas.Translate(offset);
  102. command.Cell.Draw(command.Size);
  103. Canvas.Translate(offset.Reverse());
  104. }
  105. CurrentRow = CalculateCurrentRow(renderingCommands);
  106. }
  107. private int CalculateCurrentRow(ICollection<TableCellRenderingCommand> commands)
  108. {
  109. var lastFullyRenderedRow = commands
  110. .GroupBy(x => x.Cell.Row)
  111. .Where(x => x.All(y => y.Cell.IsRendered || y.Measurement.Type is SpacePlanType.Empty or SpacePlanType.FullRender))
  112. .Select(x => x.Key)
  113. .ToArray();
  114. return lastFullyRenderedRow.Any() ? lastFullyRenderedRow.Max() + 1 : CurrentRow;
  115. }
  116. private void UpdateColumnsWidth(float availableWidth)
  117. {
  118. var constantWidth = Columns.Sum(x => x.ConstantSize);
  119. var relativeWidth = Columns.Sum(x => x.RelativeSize);
  120. var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
  121. foreach (var column in Columns)
  122. {
  123. column.Width = column.ConstantSize + column.RelativeSize * widthPerRelativeUnit;
  124. }
  125. }
  126. private ICollection<TableCellRenderingCommand> PlanLayout(Size availableSpace)
  127. {
  128. var columnOffsets = GetColumnLeftOffsets(Columns);
  129. var commands = GetRenderingCommands();
  130. if (!commands.Any())
  131. return commands;
  132. if (ExtendLastCellsToTableBottom)
  133. {
  134. var tableHeight = commands.Max(cell => cell.Offset.Y + cell.Size.Height);
  135. AdjustLastCellSizes(tableHeight, commands);
  136. }
  137. return commands;
  138. static float[] GetColumnLeftOffsets(IList<TableColumnDefinition> columns)
  139. {
  140. var cellOffsets = new float[columns.Count + 1];
  141. cellOffsets[0] = 0;
  142. foreach (var column in Enumerable.Range(1, cellOffsets.Length - 1))
  143. cellOffsets[column] = columns[column - 1].Width + cellOffsets[column - 1];
  144. return cellOffsets;
  145. }
  146. ICollection<TableCellRenderingCommand> GetRenderingCommands()
  147. {
  148. var rowBottomOffsets = new DynamicDictionary<int, float>();
  149. var commands = new List<TableCellRenderingCommand>();
  150. var cellsToTry = Enumerable
  151. .Range(CurrentRow, MaxRow - CurrentRow + 1)
  152. .SelectMany(x => CellsCache[x]);
  153. var currentRow = CurrentRow;
  154. var maxRenderingRow = RowsCount;
  155. foreach (var cell in cellsToTry)
  156. {
  157. // update position of previous row
  158. if (cell.Row > currentRow)
  159. {
  160. rowBottomOffsets[currentRow] = Math.Max(rowBottomOffsets[currentRow], rowBottomOffsets[currentRow - 1]);
  161. if (rowBottomOffsets[currentRow - 1] > availableSpace.Height + Size.Epsilon)
  162. break;
  163. foreach (var row in Enumerable.Range(cell.Row, cell.Row - currentRow))
  164. rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]);
  165. currentRow = cell.Row;
  166. }
  167. // cell visibility optimizations
  168. if (cell.Row > maxRenderingRow + MaxRowSpan)
  169. break;
  170. // calculate cell position / size
  171. var topOffset = rowBottomOffsets[cell.Row - 1];
  172. var availableWidth = GetCellWidth(cell);
  173. var availableHeight = availableSpace.Height - topOffset;
  174. var availableCellSize = new Size(availableWidth, availableHeight);
  175. var cellSize = cell.Measure(availableCellSize);
  176. // corner case: if cell within the row is not fully rendered, do not attempt to render next row
  177. if (cellSize.Type == SpacePlanType.PartialRender)
  178. {
  179. maxRenderingRow = Math.Min(maxRenderingRow, cell.Row + cell.RowSpan - 1);
  180. }
  181. // corner case: if cell within the row want to wrap to the next page, do not attempt to render this row
  182. if (cellSize.Type == SpacePlanType.Wrap)
  183. {
  184. maxRenderingRow = Math.Min(maxRenderingRow, cell.Row - 1);
  185. continue;
  186. }
  187. // update position of the last row that cell occupies
  188. var bottomRow = cell.Row + cell.RowSpan - 1;
  189. rowBottomOffsets[bottomRow] = Math.Max(rowBottomOffsets[bottomRow], topOffset + cellSize.Height);
  190. // accept cell to be rendered
  191. commands.Add(new TableCellRenderingCommand()
  192. {
  193. Cell = cell,
  194. Measurement = cellSize,
  195. Size = new Size(availableWidth, cellSize.Height),
  196. Offset = new Position(columnOffsets[cell.Column - 1], topOffset)
  197. });
  198. }
  199. if (!commands.Any())
  200. return commands;
  201. var maxRow = commands.Select(x => x.Cell).Max(x => x.Row + x.RowSpan);
  202. foreach (var row in Enumerable.Range(CurrentRow, maxRow - CurrentRow))
  203. rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]);
  204. AdjustCellSizes(commands, rowBottomOffsets);
  205. // corner case: reject cell if other cells within the same row are rejected
  206. return commands.Where(x => x.Cell.Row <= maxRenderingRow).ToList();
  207. }
  208. // corner sase: if two cells end up on the same row (a.Row + a.RowSpan = b.Row + b.RowSpan),
  209. // bottom edges of their bounding boxes should be at the same level
  210. static void AdjustCellSizes(ICollection<TableCellRenderingCommand> commands, DynamicDictionary<int, float> rowBottomOffsets)
  211. {
  212. foreach (var command in commands)
  213. {
  214. var lastRow = command.Cell.Row + command.Cell.RowSpan - 1;
  215. var height = rowBottomOffsets[lastRow] - command.Offset.Y;
  216. command.Size = new Size(command.Size.Width, height);
  217. command.Offset = new Position(command.Offset.X, rowBottomOffsets[command.Cell.Row - 1]);
  218. }
  219. }
  220. // corner sase: all cells, that are last ones in their respective columns, should take all remaining space
  221. static void AdjustLastCellSizes(float tableHeight, ICollection<TableCellRenderingCommand> commands)
  222. {
  223. var columnsCount = commands.Select(x => x.Cell).Max(x => x.Column + x.ColumnSpan - 1);
  224. foreach (var column in Enumerable.Range(1, columnsCount))
  225. {
  226. var lastCellInColumn = commands
  227. .Where(x => x.Cell.Column <= column && column < x.Cell.Column + x.Cell.ColumnSpan)
  228. .OrderByDescending(x => x.Cell.Row + x.Cell.RowSpan)
  229. .FirstOrDefault();
  230. if (lastCellInColumn == null)
  231. continue;
  232. lastCellInColumn.Size = new Size(lastCellInColumn.Size.Width, tableHeight - lastCellInColumn.Offset.Y);
  233. }
  234. }
  235. float GetCellWidth(TableCell cell)
  236. {
  237. return columnOffsets[cell.Column + cell.ColumnSpan - 1] - columnOffsets[cell.Column - 1];
  238. }
  239. }
  240. }
  241. }