Table: refactorization, cell placement algorithm
This commit is contained in:
committed by
Marcin Ziąbek
parent
82e4dd36f0
commit
d3911d5687
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
@@ -9,7 +10,7 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
public class TableExamples
|
||||
{
|
||||
public static Random Random { get; } = new Random(0);
|
||||
public static Random Random { get; } = new Random();
|
||||
|
||||
[Test]
|
||||
public void Example()
|
||||
@@ -24,8 +25,7 @@ namespace QuestPDF.Examples
|
||||
container
|
||||
.Padding(25)
|
||||
.Box()
|
||||
.Border(2)
|
||||
.MaxHeight(500)
|
||||
.Border(2)
|
||||
.Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
@@ -36,40 +36,96 @@ namespace QuestPDF.Examples
|
||||
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().ColumnSpan(2).Element(CreateBox("A"));
|
||||
table.Cell().Element(CreateBox("B"));
|
||||
table.Cell().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().Element(CreateBox("D"));
|
||||
table.Cell().RowSpan(2).Element(CreateBox("E"));
|
||||
table.Cell().RowSpan(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"));
|
||||
table.Cell().RowSpan(2).Element(CreateBox("G"));
|
||||
table.Cell().RowSpan(2).Element(CreateBox("H"));
|
||||
table.Cell().Element(CreateBox("I"));
|
||||
table.Cell().Element(CreateBox("J"));
|
||||
table.Cell().RowSpan(2).Element(CreateBox("K"));
|
||||
table.Cell().ColumnSpan(2).Element(CreateBox("L"));
|
||||
table.Cell().Element(CreateBox("M"));
|
||||
|
||||
// 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 =>
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PerformanceTest()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProducePdf()
|
||||
.PageSize(1000, 2000)
|
||||
.MaxPages(1000)
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
var height = Random.Next(2, 7) * 25;
|
||||
|
||||
container
|
||||
.Border(1)
|
||||
.Background(Placeholders.BackgroundColor())
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Border(1)
|
||||
.MinHeight(height)
|
||||
.Text($"{label}: {height}");
|
||||
};
|
||||
}
|
||||
.Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
{
|
||||
foreach (var size in Enumerable.Range(0, 10))
|
||||
columns.ConstantColumn(100);
|
||||
});
|
||||
|
||||
foreach (var i in Enumerable.Range(1, 10000))
|
||||
{
|
||||
table
|
||||
.Cell()
|
||||
.RowSpan((uint)Random.Next(1, 5))
|
||||
.ColumnSpan((uint)Random.Next(1, 5))
|
||||
.Element(CreateBox(i.ToString()));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private Action<IContainer> CreateBox(string label)
|
||||
{
|
||||
return container =>
|
||||
{
|
||||
var height = Random.Next(2, 7) * 25;
|
||||
|
||||
container
|
||||
.Border(1)
|
||||
.Background(Placeholders.BackgroundColor())
|
||||
.Layers(layers =>
|
||||
{
|
||||
layers
|
||||
.PrimaryLayer()
|
||||
.ExtendHorizontal()
|
||||
.Height(height);
|
||||
|
||||
layers
|
||||
.Layer()
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text($"{label}: {height}px");
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
public interface ITableCellContainer : IContainer
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,11 @@
|
||||
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
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
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>();
|
||||
@@ -212,24 +168,4 @@ namespace QuestPDF.Elements
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
internal class TableCellRenderingCommand
|
||||
{
|
||||
public TableCell Cell { get; set; }
|
||||
public Size Size { get; set; }
|
||||
public Position Offset { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuestPDF.Fluent;
|
||||
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
static class TableLayoutPlanner
|
||||
{
|
||||
public static void PlanCellPositions(this Table table)
|
||||
{
|
||||
PlanCellPositions(table.Columns.Count, table.Children);
|
||||
}
|
||||
|
||||
private static void PlanCellPositions(int columnsCount, ICollection<TableCell> cells)
|
||||
{
|
||||
var cellsWindow = new List<TableCell>();
|
||||
(int x, int y) currentLocation = (1, 1);
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (cellsWindow.Count > Math.Max(columnsCount, 16))
|
||||
{
|
||||
cellsWindow = cellsWindow
|
||||
.Where(x => x.Row + x.RowSpan > currentLocation.y)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
if (cell.HasLocation())
|
||||
{
|
||||
cellsWindow.Add(cell);
|
||||
currentLocation = (cell.Column, cell.Row);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var location in GenerateCoordinates(columnsCount, currentLocation))
|
||||
{
|
||||
cell.Column = location.x;
|
||||
cell.Row = location.y;
|
||||
|
||||
if (cell.CollidesWithAnyOf(cellsWindow))
|
||||
continue;
|
||||
|
||||
cellsWindow.Add(cell);
|
||||
currentLocation = (cell.Column, cell.Row);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<(int x, int y)> GenerateCoordinates(int columnsCount, (int x, int y) startPosition)
|
||||
{
|
||||
if (startPosition.x > columnsCount)
|
||||
throw new ArgumentException();
|
||||
|
||||
foreach (var x in Enumerable.Range(startPosition.x, columnsCount - startPosition.x + 1))
|
||||
yield return (x, startPosition.y);
|
||||
|
||||
foreach (var y in Enumerable.Range(startPosition.y + 1, 1_000_000))
|
||||
foreach (var x in Enumerable.Range(1, columnsCount))
|
||||
yield return (x, y);
|
||||
}
|
||||
|
||||
private static bool CollidesWith(this TableCell cell, TableCell neighbour)
|
||||
{
|
||||
return cell.Column < neighbour.Column + neighbour.ColumnSpan &&
|
||||
cell.Column + cell.ColumnSpan > neighbour.Column &&
|
||||
cell.Row < neighbour.Row + neighbour.RowSpan &&
|
||||
cell.RowSpan + cell.Row > neighbour.Row;
|
||||
}
|
||||
|
||||
private static bool CollidesWithAnyOf(this TableCell cell, ICollection<TableCell> neighbours)
|
||||
{
|
||||
return neighbours.Any(cell.CollidesWith);
|
||||
}
|
||||
|
||||
private static bool HasLocation(this TableCell cell)
|
||||
{
|
||||
return cell.Row != 1 && cell.Column != 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Table
|
||||
{
|
||||
internal class TableRenderingPlan
|
||||
{
|
||||
public Size Size { get; set; }
|
||||
public List<TableCellRenderingCommand> CellRenderingCommands { get; set; }
|
||||
public int MaxRowRendered { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Elements.Table;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
@@ -50,7 +52,7 @@ namespace QuestPDF.Fluent
|
||||
Table.Spacing = value;
|
||||
}
|
||||
|
||||
public ITableCellContainer Cell()
|
||||
public ITableCellContainer Cell(int row = 1, int column = 1, int rowSpan = 1, int columnsSpan = 1)
|
||||
{
|
||||
var cell = new TableCell();
|
||||
Table.Children.Add(cell);
|
||||
@@ -66,6 +68,8 @@ namespace QuestPDF.Fluent
|
||||
var descriptor = new TableDescriptor(table);
|
||||
|
||||
handler(descriptor);
|
||||
table.PlanCellPositions();
|
||||
|
||||
element.Element(table);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user