Improved table paging support

This commit is contained in:
Marcin Ziąbek
2021-12-19 05:11:23 +01:00
parent 561d6a004c
commit 82e4dd36f0
5 changed files with 417 additions and 3 deletions
+75
View File
@@ -0,0 +1,75 @@
using System;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class TableExamples
{
public static Random Random { get; } = new Random(0);
[Test]
public void Example()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(PageSizes.A4)
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.Box()
.Border(2)
.MaxHeight(500)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(100);
columns.RelativeColumn();
columns.ConstantColumn(100);
columns.ConstantColumn(200);
});
table.Cell().Row(1).Column(1).ColumnSpan(2).Element(CreateBox("A"));
table.Cell().Row(1).Column(3).Element(CreateBox("B"));
table.Cell().Row(1).Column(4).Element(CreateBox("C"));
table.Cell().Row(2).Column(1).Element(CreateBox("D"));
table.Cell().Row(2).RowSpan(2).Column(2).Element(CreateBox("E"));
table.Cell().Row(2).RowSpan(3).Column(3).ColumnSpan(2).Element(CreateBox("F"));
table.Cell().Row(3).RowSpan(2).Column(1).Element(CreateBox("G"));
table.Cell().Row(4).RowSpan(2).Column(2).Element(CreateBox("H"));
table.Cell().Row(5).Column(3).Element(CreateBox("I"));
table.Cell().Row(5).Column(4).Element(CreateBox("J"));
table.Cell().Row(5).RowSpan(2).Column(1).Element(CreateBox("K"));
table.Cell().Row(6).Column(2).ColumnSpan(2).Element(CreateBox("L"));
table.Cell().Row(6).Column(4).Element(CreateBox("M"));
});
});
Action<IContainer> CreateBox(string label)
{
return container =>
{
var height = Random.Next(2, 7) * 25;
container
.Border(1)
.Background(Placeholders.BackgroundColor())
.AlignCenter()
.AlignMiddle()
.Border(1)
.MinHeight(height)
.Text($"{label}: {height}");
};
}
}
}
}
+3 -3
View File
@@ -8,8 +8,8 @@ namespace QuestPDF.Elements
{
internal class RowElement : Constrained
{
public float ConstantSize { get; set; }
public float RelativeSize { get; set; }
public float ConstantSize { get; }
public float RelativeSize { get; }
public RowElement(float constantSize, float relativeSize)
{
@@ -138,7 +138,7 @@ namespace QuestPDF.Elements
var constantWidth = Children.Sum(x => x.ConstantSize);
var relativeWidth = Children.Sum(x => x.RelativeSize);
var widthPerRelativeUnit = (availableWidth - constantWidth) / relativeWidth;
var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
foreach (var row in Children)
{
+235
View File
@@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class TableColumnDefinition
{
public float ConstantSize { get; }
public float RelativeSize { get; }
internal float Width { get; set; }
public TableColumnDefinition(float constantSize, float relativeSize)
{
ConstantSize = constantSize;
RelativeSize = relativeSize;
}
}
public interface ITableCellContainer : IContainer
{
}
internal class TableCell : Container, ITableCellContainer
{
public int Row { get; set; } = 1;
public int RowSpan { get; set; } = 1;
public int Column { get; set; } = 1;
public int ColumnSpan { get; set; } = 1;
}
internal class TableRenderingPlan
{
public Size Size { get; set; }
public List<TableCellRenderingCommand> CellRenderingCommands { get; set; }
public int MaxRowRendered { get; set; }
}
internal class TableCellRenderingCommand
{
public TableCell Cell { get; set; }
public Size Size { get; set; }
public Position Offset { get; set; }
}
internal class Table : Element, IStateResettable
{
public ICollection<TableColumnDefinition> Columns { get; } = new List<TableColumnDefinition>();
public ICollection<TableCell> Children { get; } = new List<TableCell>();
public float Spacing { get; set; }
public int CurrentRow { get; set; }
internal override void HandleVisitor(Action<Element?> visit)
{
Children.ToList().ForEach(x => x.HandleVisitor(visit));
base.HandleVisitor(visit);
}
public void ResetState()
{
CurrentRow = 1;
}
internal override SpacePlan Measure(Size availableSpace)
{
UpdateColumnsWidth(availableSpace.Width);
var layout = PlanLayout(availableSpace);
return layout.MaxRowRendered < GetRowsCount()
? SpacePlan.PartialRender(layout.Size)
: SpacePlan.FullRender(layout.Size);
}
internal override void Draw(Size availableSpace)
{
UpdateColumnsWidth(availableSpace.Width);
var layout = PlanLayout(availableSpace);
CurrentRow = layout.MaxRowRendered;
foreach (var command in layout.CellRenderingCommands)
{
Canvas.Translate(command.Offset);
command.Cell.Draw(command.Size);
Canvas.Translate(command.Offset.Reverse());
}
}
private void UpdateColumnsWidth(float availableWidth)
{
var constantWidth = Columns.Sum(x => x.ConstantSize);
var relativeWidth = Columns.Sum(x => x.RelativeSize);
var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
foreach (var column in Columns)
{
column.Width = column.ConstantSize + column.RelativeSize * widthPerRelativeUnit;
}
}
private TableRenderingPlan PlanLayout(Size availableSpace)
{
var cellRenderingCommands = new List<TableCellRenderingCommand>();
// update row heights
var rowsCount = GetRowsCount();
var rowBottomOffsets = new float[rowsCount];
var childrenToTry = Children.Where(x => x.Row >= CurrentRow).OrderBy(x => x.Row);
foreach (var child in childrenToTry)
{
var rowIndex = child.Row - 1;
var topOffset = 0f;
if (rowIndex > 0)
topOffset = rowBottomOffsets[rowIndex - 1]; // look at previous row
var height = GetCellSize(child).Height;
var cellBottomOffset = height + topOffset;
var targetRowId = child.Row + child.RowSpan - 2; // -1 because indexing starts at 0, -1 because rowSpan starts at 1
rowBottomOffsets[targetRowId] = Math.Max(rowBottomOffsets[targetRowId], cellBottomOffset);
}
Enumerable
.Range(1, rowsCount - 1)
.ToList()
.ForEach(x => rowBottomOffsets[x] = Math.Max(rowBottomOffsets[x], rowBottomOffsets[x-1]));
var rowHeights = new float[rowsCount];
rowHeights[0] = rowBottomOffsets[0];
Enumerable
.Range(1, rowsCount - 1)
.ToList()
.ForEach(x => rowHeights[x] = rowBottomOffsets[x] - rowBottomOffsets[x-1]);
// find rows count to render in this pass
var rowsToDisplay = rowHeights.Scan((x, y) => x + y).Count(x => x <= availableSpace.Height + Size.Epsilon);
rowHeights = rowHeights.Take(rowsToDisplay).ToArray();
var totalHeight = rowHeights.Sum();
var totalWidth = Columns.Sum(x => x.Width);
foreach (var cell in Children)
{
if (!IsCellVisible(cell, rowsToDisplay))
continue;
var leftOffset = GetCellWidthOffset(cell);
var topOffset = rowHeights.Take(cell.Row - 1).Sum();
var width = GetCellWidth(cell);
var height = rowHeights.Skip(cell.Row - 1).Take(cell.RowSpan).Sum();
cellRenderingCommands.Add(new TableCellRenderingCommand()
{
Cell = cell,
Size = new Size(width, height),
Offset = new Position(leftOffset, topOffset)
});
}
return new TableRenderingPlan
{
Size = new Size(totalWidth, totalHeight),
CellRenderingCommands = cellRenderingCommands,
MaxRowRendered = rowsToDisplay
};
float GetCellWidthOffset(TableCell cell)
{
return Columns.Take(cell.Column - 1).Select(x => x.Width).DefaultIfEmpty(0).Sum();
}
float GetCellWidth(TableCell cell)
{
return Columns.Skip(cell.Column - 1).Take(cell.ColumnSpan).Sum(x => x.Width);
}
Size GetCellSize(TableCell cell)
{
var width = GetCellWidth(cell);
var cellSize = new Size(width, Size.Max.Height);
var measurement = cell.Measure(cellSize);
if (measurement.Type == SpacePlanType.Wrap)
return new Size(width, Size.Infinity);
return measurement;
}
bool IsCellVisible(TableCell cell, int maxRow)
{
return cell.Row <= maxRow;
}
}
int GetRowsCount()
{
return Children.Max(x => x.Row + x.RowSpan);
}
}
internal static class EnumerableExtensions
{
public static IEnumerable<T> Scan<T>(this IEnumerable<T> input, Func<T, T, T> accumulate)
{
using var enumerator = input.GetEnumerator();
if (!enumerator.MoveNext())
yield break;
var state = enumerator.Current;
yield return state;
while (enumerator.MoveNext())
{
state = accumulate(state, enumerator.Current);
yield return state;
}
}
}
}
+103
View File
@@ -0,0 +1,103 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class TableDefinitionDescriptor
{
private Table Table { get; }
internal TableDefinitionDescriptor(Table table)
{
Table = table;
}
public void ConstantColumn(float width)
{
ComplexColumn(constantWidth: width);
}
public void RelativeColumn(float width = 1)
{
ComplexColumn(relativeWidth: width);
}
public void ComplexColumn(float constantWidth = 0, float relativeWidth = 0)
{
var columnDefinition = new TableColumnDefinition(constantWidth, relativeWidth);
Table.Columns.Add(columnDefinition);
}
}
public class TableDescriptor
{
private Table Table { get; }
internal TableDescriptor(Table table)
{
Table = table;
}
public void ColumnsDefinition(Action<TableDefinitionDescriptor> handler)
{
var descriptor = new TableDefinitionDescriptor(Table);
handler(descriptor);
}
public void Spacing(float value)
{
Table.Spacing = value;
}
public ITableCellContainer Cell()
{
var cell = new TableCell();
Table.Children.Add(cell);
return cell;
}
}
public static class TableExtensions
{
public static void Table(this IContainer element, Action<TableDescriptor> handler)
{
var table = new Table();
var descriptor = new TableDescriptor(table);
handler(descriptor);
element.Element(table);
}
}
public static class TableCellExtensions
{
private static ITableCellContainer TableCell(this ITableCellContainer element, Action<TableCell> handler)
{
if (element is TableCell tableCell)
handler(tableCell);
return element;
}
public static ITableCellContainer Column(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.Column = (int)value);
}
public static ITableCellContainer ColumnSpan(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.ColumnSpan = (int)value);
}
public static ITableCellContainer Row(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.Row = (int)value);
}
public static ITableCellContainer RowSpan(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.RowSpan = (int)value);
}
}
}
+1
View File
@@ -3,6 +3,7 @@
public readonly struct Size
{
public const float Epsilon = 0.001f;
public const float Infinity = float.PositiveInfinity;
public readonly float Width;
public readonly float Height;